How do I correctly to structure a GitHub Actions workflow that executes terraform plan on pull requests and terraform apply only upon merge to main? I need to securely handle the Genesys Cloud Terraform provider credentials without exposing secrets in logs. My current setup fails because the state file is not persisted between jobs. Below is my workflow snippet. How do I inject the GC_OAUTH_CLIENT_ID and GC_OAUTH_CLIENT_SECRET correctly for the plan step?
If I remember correctly, you need to split the authentication and state handling into distinct steps using a backend that supports locking, like S3 or Terraform Cloud, rather than relying on local state in ephemeral jobs.
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: hashicorp/setup-terraform@v2
- name: Init
run: terraform init -backend-config=backend.hcl
- name: Plan
env:
GC_OAUTH_CLIENT_ID: ${{ secrets.GC_OAUTH_CLIENT_ID }}
GC_OAUTH_CLIENT_SECRET: ${{ secrets.GC_OAUTH_CLIENT_SECRET }}
run: terraform plan -out=tfplan
apply:
needs: plan
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: hashicorp/setup-terraform@v2
- name: Init
run: terraform init -backend-config=backend.hcl
- name: Apply
env:
GC_OAUTH_CLIENT_ID: ${{ secrets.GC_OAUTH_CLIENT_ID }}
GC_OAUTH_CLIENT_SECRET: ${{ secrets.GC_OAUTH_CLIENT_SECRET }}
run: terraform apply -auto-approve tfplan
This ensures secrets are injected via GitHub Secrets and state is centralized, avoiding the persistence issue you described.
Check your state backend configuration because ephemeral runners discard local state files immediately after job completion, which is why your plan fails to recognize previous resource tracking. You need a remote backend like S3 with DynamoDB for locking to persist state across the plan and apply jobs.
The suggestion above regarding S3 is correct, but you must also handle the Genesys Cloud provider authentication explicitly in each job to avoid credential leakage in logs. Do not pass GC_OAUTH_CLIENT_ID and GC_OAUTH_CLIENT_SECRET as environment variables directly to the terraform command if they appear in verbose output. Instead, use a terraform.tfvars file generated at runtime from GitHub Secrets.
Here is the corrected workflow structure. It uses backend.hcl for S3 state persistence and generates credentials dynamically:
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: hashicorp/setup-terraform@v2
- name: Configure Backend
run: |
echo "bucket = \"my-terraform-state\"" > backend.hcl
echo "key = \"genesys/terraform.tfstate\"" >> backend.hcl
echo "region = \"us-east-1\"" >> backend.hcl
- name: Init
run: terraform init -backend-config=backend.hcl
- name: Plan
env:
TF_VAR_client_id: ${{ secrets.GC_OAUTH_CLIENT_ID }}
TF_VAR_client_secret: ${{ secrets.GC_OAUTH_CLIENT_SECRET }}
run: terraform plan -out=tfplan
- uses: actions/upload-artifact@v3
with:
name: tfplan
path: tfplan
apply:
needs: plan
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
name: tfplan
- uses: hashicorp/setup-terraform@v2
- name: Init
run: terraform init -backend-config=backend.hcl
- name: Apply
env:
TF_VAR_client_id: ${{ secrets.GC_OAUTH_CLIENT_ID }}
TF_VAR_client_secret: ${{ secrets.GC_OAUTH_CLIENT_SECRET }}
run: terraform apply -auto-approve tfplan
This approach ensures state consistency and keeps secrets out of the plan output logs.
If I remember correctly… you are overcomplicating the auth layer. Injecting client credentials via env vars in the workflow is standard, but the real issue is state persistence.
use S3 with DynamoDB locking for the backend. here is the minimal backend.hcl and job structure:
# backend.hcl
bucket = "my-gc-tf-state"
key = "prod/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "terraform-lock"
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: hashicorp/setup-terraform@v2
- run: terraform init -backend-config=backend.hcl
- run: terraform plan -out=tfplan
env:
GC_OAUTH_CLIENT_ID: ${{ secrets.GC_CLIENT_ID }}
GC_OAUTH_CLIENT_SECRET: ${{ secrets.GC_CLIENT_SECRET }}
- uses: actions/upload-artifact@v3
with:
name: tfplan
path: tfplan
Ah, this is a recognized issue…
- Ensure
terraform initruns in bothplanandapplyjobs to sync remote state from S3. - Use
${{ secrets.GC_OAUTH_CLIENT_ID }}directly inenvblocks, never echo them. - Verify
backend.hclmatches the exact S3 bucket and DynamoDB table configured in your AWS account.