Automate Genesys Cloud Infrastructure with Terraform and GitHub Actions
What You Will Build
- This tutorial builds a GitHub Actions workflow that executes
terraform planon pull requests andterraform applyon merges to the main branch. - It uses the Genesys Cloud Terraform Provider and GitHub Actions secrets for secure authentication.
- The implementation covers Python-based token generation scripts and Bash-driven Terraform execution.
Prerequisites
- GitHub Repository: A repository initialized with Terraform configuration files.
- Genesys Cloud Environment: An environment with API access (Sandbox or Production).
- OAuth Client Credentials: A Genesys Cloud OAuth client with the
adminrole or specific scopes required for your resources (e.g.,user:read,routing:write). - Terraform: Version 1.0+ installed in the GitHub Actions runner environment.
- GitHub Actions: Enabled on your repository.
Authentication Setup
Genesys Cloud APIs require OAuth 2.0 authentication. For Terraform, the provider supports passing a token directly or using a client ID and secret to generate one dynamically. In a CI/CD pipeline, storing long-lived tokens is a security risk. Instead, we will use the Client Credentials Grant flow to generate a short-lived access token at runtime.
Step 1: Create a Token Generation Script
We will use a Python script to handle the OAuth flow. This script reads the client ID and secret from environment variables, requests a token from the Genesys Cloud OAuth endpoint, and prints the token to stdout. Terraform will capture this output.
Create a file named gen_token.py in your repository root.
import os
import sys
import requests
def get_genesys_token():
"""
Generates a Genesys Cloud OAuth access token using Client Credentials Grant.
Returns:
str: The access token if successful, otherwise exits with an error.
"""
# Environment variables set by GitHub Actions
client_id = os.environ.get("GENESYS_CLIENT_ID")
client_secret = os.environ.get("GENESYS_CLIENT_SECRET")
environment = os.environ.get("GENESYS_ENVIRONMENT", "mypurecloud.com")
if not client_id or not client_secret:
print("Error: GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set.", file=sys.stderr)
sys.exit(1)
# Construct the OAuth endpoint
# For US: https://api.mypurecloud.com
# For EU: https://api.eu.mypurecloud.com
# For AU: https://api.ap.mypurecloud.com
base_url = f"https://api.{environment}"
token_url = f"{base_url}/oauth/token"
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
payload = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret
}
try:
response = requests.post(token_url, headers=headers, data=payload, timeout=10)
# Check for HTTP errors
response.raise_for_status()
token_data = response.json()
access_token = token_data.get("access_token")
if not access_token:
raise Exception("Token response did not contain an access_token")
print(access_token)
except requests.exceptions.RequestException as e:
print(f"Error fetching token: {e}", file=sys.stderr)
sys.exit(1)
except ValueError as e:
print(f"Error parsing JSON response: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
get_genesys_token()
Step 2: Configure Terraform Provider
Update your main.tf to accept the token via an environment variable. We will use the genesyscloud provider.
terraform {
required_providers {
genesyscloud = {
source = "mikesparr/genesyscloud"
version = ">= 1.0.0"
}
}
}
provider "genesyscloud" {
# The token is injected via the GENESYS_CLOUD_ACCESS_TOKEN environment variable
# This is set in the GitHub Actions workflow before running terraform
}
Implementation
Step 1: Define GitHub Secrets
Before writing the workflow, you must store your credentials securely in GitHub.
- Navigate to your repository Settings > Secrets and variables > Actions.
- Add the following secrets:
GENESYS_CLIENT_ID: Your Genesys Cloud OAuth Client ID.GENESYS_CLIENT_SECRET: Your Genesys Cloud OAuth Client Secret.GENESYS_ENVIRONMENT: Optional. Defaults tomypurecloud.com. Useeu.mypurecloud.comfor Europe.
Step 2: Create the GitHub Actions Workflow
Create a file at .github/workflows/terraform.yml. This workflow triggers on push and pull_request events.
name: Terraform Genesys Cloud Pipeline
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
terraform:
name: Terraform Plan and Apply
runs-on: ubuntu-latest
# Environment variables for the job
env:
GENESYS_ENVIRONMENT: ${{ secrets.GENESYS_ENVIRONMENT || 'mypurecloud.com' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install Python Dependencies
run: |
pip install requests
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.5.0
- name: Generate Genesys Cloud Token
id: gen-token
env:
GENESYS_CLIENT_ID: ${{ secrets.GENESYS_CLIENT_ID }}
GENESYS_CLIENT_SECRET: ${{ secrets.GENESYS_CLIENT_SECRET }}
run: |
# Run the Python script and store the token in a variable
TOKEN=$(python gen_token.py)
# Mask the token in the logs to prevent leakage
echo "::add-mask::$TOKEN"
# Export the token as an environment variable for subsequent steps
echo "GENESYS_CLOUD_ACCESS_TOKEN=$TOKEN" >> $GITHUB_ENV
- name: Terraform Init
run: terraform init
- name: Terraform Format Check
run: terraform fmt -check -diff
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
if: github.event_name == 'pull_request'
id: plan
run: |
terraform plan -out=tfplan -input=false
# Save the plan output to a file for display in PR comments
terraform show -json tfplan > plan.json
- name: Terraform Apply
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: |
terraform apply -auto-approve tfplan
Step 3: Handle State Management
Terraform requires a backend to store state. For a CI/CD pipeline, you must use a remote backend. Genesys Cloud does not provide a native Terraform backend, so you should use AWS S3, Azure Blob Storage, or Terraform Cloud.
Here is an example using AWS S3:
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "genesys-cx/terraform.tfstate"
region = "us-east-1"
}
# ... required_providers ...
}
You must also configure AWS credentials in GitHub Actions secrets (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) and add them to the workflow environment.
Complete Working Example
Below is the complete terraform.yml workflow file with S3 backend support and enhanced error handling.
name: Terraform Genesys Cloud Pipeline
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
terraform:
name: Terraform Operations
runs-on: ubuntu-latest
env:
GENESYS_ENVIRONMENT: ${{ secrets.GENESYS_ENVIRONMENT || 'mypurecloud.com' }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install Python Dependencies
run: pip install requests
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.5.0
- name: Generate Genesys Cloud Token
id: gen-token
env:
GENESYS_CLIENT_ID: ${{ secrets.GENESYS_CLIENT_ID }}
GENESYS_CLIENT_SECRET: ${{ secrets.GENESYS_CLIENT_SECRET }}
run: |
TOKEN=$(python gen_token.py)
echo "::add-mask::$TOKEN"
echo "GENESYS_CLOUD_ACCESS_TOKEN=$TOKEN" >> $GITHUB_ENV
- name: Terraform Init
run: terraform init
- name: Terraform Format Check
run: terraform fmt -check -diff
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
if: github.event_name == 'pull_request'
id: plan
run: |
# Create the plan file
terraform plan -out=tfplan -input=false
# Generate a human-readable plan for the PR comment
terraform show -json tfplan > plan.json
# Upload the plan as an artifact for debugging if needed
upload-artifact --name=tfplan --file=tfplan
- name: Terraform Apply
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: |
# Apply the plan that was created in the previous step (if any)
# Note: In a push event, we typically re-init and apply from scratch or use a saved plan
# For simplicity, we re-run plan and apply in one go here, or apply a previously saved plan
terraform apply -auto-approve -input=false
- name: Comment Plan on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const planOutput = fs.readFileSync('plan.json', 'utf8');
const summary = planOutput.substring(0, 500) + '...';
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `### Terraform Plan Summary\n\`\`\`json\n${summary}\n\`\`\`\n\nFull plan uploaded as artifact.`
});
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token is invalid, expired, or the client credentials are incorrect.
- Fix: Verify that
GENESYS_CLIENT_IDandGENESYS_CLIENT_SECRETare correct in GitHub Secrets. Ensure the Python script is successfully printing the token. Check the logs for the::add-mask::step to confirm a token was generated.
Error: 403 Forbidden
- Cause: The OAuth client lacks the necessary scopes to perform the action (e.g., creating a user requires
user:write). - Fix: In Genesys Cloud Admin > Integrations > OAuth Clients, edit your client and add the required scopes. Common scopes include
admin,user:read,routing:write,organization:read.
Error: Terraform State Lock
- Cause: Another process is holding the state lock in S3.
- Fix: Check if another workflow is running. If the lock is stale, you can force-unlock it using
terraform force-unlock <LOCK_ID>.
Error: Python Script Fails to Import Requests
- Cause: The
requestslibrary is not installed in the GitHub Actions runner. - Fix: Ensure the step
pip install requestsis present in the workflow before running the Python script.