Automate Genesys Cloud Infrastructure with Terraform and GitHub Actions

Automate Genesys Cloud Infrastructure with Terraform and GitHub Actions

What You Will Build

  • This tutorial builds a GitHub Actions workflow that executes terraform plan on pull requests and terraform apply on 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 admin role 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.

  1. Navigate to your repository Settings > Secrets and variables > Actions.
  2. 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 to mypurecloud.com. Use eu.mypurecloud.com for 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_ID and GENESYS_CLIENT_SECRET are 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 requests library is not installed in the GitHub Actions runner.
  • Fix: Ensure the step pip install requests is present in the workflow before running the Python script.

Official References