You’re right to worry about state locking. If your plan runs on a PR branch but doesn’t have exclusive access to the state file, you’ll get conflicts. The trick isn’t just about the workflow steps, it’s about how you scope the state.
For Genesys Cloud, I’d strongly recommend splitting your state files by deployment unit or even by resource type if they’re loosely coupled. But assuming you want a single state file for now, you need to ensure the PR run is read-only regarding state locks where possible, or that the lock is released immediately.
Here is a pattern that works well with GitHub Actions and the Genesys Cloud provider. The key is setting GENESYS_CLOUD_OAUTH_CLIENT_ID and GENESYS_CLOUD_OAUTH_CLIENT_SECRET as repository secrets, not environment variables in the workflow file itself.
name: Genesys Cloud Terraform CI/CD
on:
pull_request:
branches: [ main ]
paths:
- '**/*.tf'
push:
branches: [ main ]
paths:
- '**/*.tf'
jobs:
terraform-plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.5.0
- name: Init
run: terraform init -backend-config="access_key=${{ secrets.AWS_ACCESS_KEY_ID }}" -backend-config="secret_key=${{ secrets.AWS_SECRET_ACCESS_KEY }}"
- name: Plan
id: plan
run: terraform plan -no-color -var="oauth_client_id=${{ secrets.GENESYS_CLOUD_OAUTH_CLIENT_ID }}" -var="oauth_client_secret=${{ secrets.GENESYS_CLOUD_OAUTH_CLIENT_SECRET }}" -out=tfplan
env:
TF_IN_AUTOMATION: true
TF_INPUT: false
- name: Upload Plan
if: always()
uses: actions/upload-artifact@v3
with:
name: tfplan
path: tfplan
- name: Comment Plan on PR
if: always()
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const plan = fs.readFileSync('tfplan', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '## Terraform Plan Output\n```' + plan + '```'
})
terraform-apply:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: terraform-plan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.5.0
- name: Init
run: terraform init -backend-config="access_key=${{ secrets.AWS_ACCESS_KEY_ID }}" -backend-config="secret_key=${{ secrets.AWS_SECRET_ACCESS_KEY }}"
- name: Apply
run: terraform apply -auto-approve -var="oauth_client_id=${{ secrets.GENESYS_CLOUD_OAUTH_CLIENT_ID }}" -var="oauth_client_secret=${{ secrets.GENESYS_CLOUD_OAUTH_CLIENT_SECRET }}"
env:
TF_IN_AUTOMATION: true
TF_INPUT: false
One thing to watch out for is the terraform init step. If you’re using a remote backend like S3 or Terraform Cloud, make sure the credentials for the backend are separate from the Genesys Cloud API credentials. Mixing them up causes confusing errors.
Also, Genesys Cloud APIs can be rate-limited. If your plan is huge, you might hit limits. You can add a retry logic in the apply step if needed, but usually, the provider handles basic retries. Just make sure your OAuth client has the right scopes. admin:all is easiest for testing, but for production, you should narrow it down to routing:all, organization:all, etc.
State locking is handled by the backend. If you’re using S3 with DynamoDB, the lock is automatic. If you’re using local state, you’re going to have a bad time with CI/CD. Don’t use local state for shared projects.
The comment on PR step is really useful for reviewers. They can see exactly what will change without running terraform locally. It helps catch accidental deletions of queues or users.
One more thing. If you have multiple environments (dev, staging, prod), you’ll want to separate the state files completely. Maybe use dev.tfstate and prod.tfstate. You can do this with different backend configs or different directories. Mixing environments in one state file is a recipe for disaster.
I’ve seen teams try to use variables to switch environments within one state file. It works until it doesn’t. When you delete a resource in dev, it might try to delete it in prod if the IDs match up weirdly. Just keep them separate.
The workflow above is a good starting point. You can tweak the conditions based on your branch protection rules. Make sure you require the plan job to pass before merging. That way, you never apply broken infrastructure.
Also, check your Terraform provider version. Genesys Cloud updates their APIs frequently. An old provider might miss new features or break on API changes. Pin your provider version in versions.tf so you know exactly what you’re running.
terraform {
required_providers {
genesyscloud = {
source = "mygenesys/genesyscloud"
version = "~> 1.0"
}
}
}
This keeps things stable. You can update the version when you’re ready to test new features.
If you run into issues with the plan output being too long for the comment, you can truncate it or upload it as an artifact and link to it. GitHub comments have a character limit.
Hope this helps you get your pipeline running. It’s a bit of setup, but it pays off when you can push changes without worrying about manual errors.