Terraform CI/CD: Running plan on PR and apply on merge for Genesys Cloud

Hey folks,

I’m trying to get our Terraform workflow set up properly for the Genesys Cloud provider. We want a standard CI/CD flow where terraform plan runs automatically when a PR is opened, and terraform apply triggers only after the PR is merged into main.

The tricky part is handling the OAuth credentials securely in the pipeline environment without hardcoding them. I’ve been using environment variables for the GENESYS_CLOUD_OAUTH_CLIENT_ID and GENESYS_CLOUD_OAUTH_CLIENT_SECRET, but I’m running into issues with the token expiration during long-running plans.

Here’s the basic structure of my GitHub Actions workflow:

name: Terraform CI/CD
on:
 pull_request:
 paths:
 - '.github/workflows/terraform.yml'
 - 'terraform/**'
 push:
 branches:
 - main

jobs:
 terraform:
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v3
 
 - name: Setup Terraform
 uses: hashicorp/setup-terraform@v2
 
 - name: Terraform Init
 run: terraform init
 
 - name: Terraform Plan
 if: github.event_name == 'pull_request'
 run: terraform plan -out=tfplan
 env:
 GENESYS_CLOUD_OAUTH_CLIENT_ID: ${{ secrets.GENESYS_CLIENT_ID }}
 GENESYS_CLOUD_OAUTH_CLIENT_SECRET: ${{ secrets.GENESYS_CLIENT_SECRET }}
 GENESYS_CLOUD_OAUTH_GRANT_TYPE: client_credentials
 GENESYS_CLOUD_OAUTH_JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }}
 
 - name: Terraform Apply
 if: github.ref == 'refs/heads/main'
 run: terraform apply -auto-approve tfplan
 env:
 GENESYS_CLOUD_OAUTH_CLIENT_ID: ${{ secrets.GENESYS_CLIENT_ID }}
 GENESYS_CLOUD_OAUTH_CLIENT_SECRET: ${{ secrets.GENESYS_CLIENT_SECRET }}
 GENESYS_CLOUD_OAUTH_GRANT_TYPE: client_credentials
 GENESYS_CLOUD_OAUTH_JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }}

The plan step works fine on the PR, but when I try to apply on merge, it fails because the state file isn’t persisted between the PR check and the merge action. I’m storing state in a local backend for now, which obviously doesn’t work across different workflow runs.

Should I be using a remote backend like S3 or Terraform Cloud? And how do I handle the state locking so two people don’t apply at the same time? Also, is there a way to cache the OAuth token between steps so it doesn’t time out?

Any advice on setting up the remote state backend securely?

  • Stop passing raw client IDs and secrets into your CI environment variables. That’s a security nightmare waiting to happen. Use the genesyscloud vider’s oauth_client_credentials block with a secret manager integration instead. Here’s how I handle it in our Azure DevOps pipelines.
  • First, store the client ID and secret in Azure Key Vault or AWS Secrets Manager. Then, inject them as masked variables into the Terraform run. The vider config looks like this:
vider "genesyscloud" {
 oauth_client_credentials {
 client_id = var.genesys_client_id
 client_secret = var.genesys_client_secret
 }
}
  • For the CI/CD split, don’t run apply on the merge commit directly. Use a separate pipeline stage that triggers only on main branch pushes. This keeps the plan output visible in the PR comments for review. I use the tf-plan GitHub action for the PR stage and a standard terraform apply step for the merge stage.
  • Watch out for the state file locking. If your PR plan and the main apply step hit the same backend at the same time, you’ll get lock errors. Make sure your backend config uses a distinct lock table or path for the plan phase if you’re using a shared state file. Or just use the tfplan action which handles the state lock gracefully for read-only operations.
  • Finally, wrap the apply step in a manual approval gate if you’re touching duction resources. Even with a clean plan, the Genesys API can be quirky with dependent resources like routing queues and users. Better to pause and verify than to trigger a cascade of 409 conflicts.