Implementing Version-Controlled Terraform Modules for Genesys Cloud Queue and Skill Configs

Implementing Version-Controlled Terraform Modules for Genesys Cloud Queue and Skill Configs

What This Guide Covers

You are implementing Infrastructure as Code (IaC) for your Genesys Cloud contact center configuration using the official Genesys Cloud Terraform provider. When complete, your queue configurations, skill definitions, routing policies, and wrap-up code sets will be declared as Terraform HCL resources in version-controlled modules, allowing all contact center configuration changes to flow through a standard Git PR → Plan → Apply workflow-providing auditability, change history, a single source of truth, and disaster recovery for your Genesys Cloud configuration state.


Prerequisites, Roles & Licensing

  • Genesys Cloud: Any CX tier.
  • Permissions required:
    • Routing > Queue > Add/Edit/Delete
    • Routing > Skill > Add/Edit/Delete
    • OAuth > Client > Create (for the Terraform service account)
  • Infrastructure:
    • Terraform CLI 1.5+ installed.
    • The genesyscloud Terraform provider (from the Terraform Registry).
    • A remote state backend (AWS S3 + DynamoDB state locking, or Terraform Cloud).

The Implementation Deep-Dive

1. The Configuration Drift Problem

Contact center configurations mutate constantly:

  • A new queue is created manually via the Genesys Cloud Admin UI.
  • A skill is renamed by an ops team member.
  • A wrap-up code is deleted that 3 different flows depended on.
  • Six months later, nobody knows what the “correct” state is.

Terraform solves this by making the Git repository the authoritative source of truth. Any configuration not declared in Terraform is drifted - and can be detected automatically.


2. Provider Configuration

# providers.tf
terraform {
  required_providers {
    genesyscloud = {
      source  = "MyPureCloud/genesyscloud"
      version = "~> 1.32.0"
    }
  }
  
  backend "s3" {
    bucket         = "your-terraform-state-bucket"
    key            = "genesys-cloud/contact-center.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-state-lock"
  }
}

provider "genesyscloud" {
  oauthclient_id     = var.genesys_client_id
  oauthclient_secret = var.genesys_client_secret
  aws_region         = "us-east-1"  # Your Genesys Cloud region
}

3. Skill Module

Create a reusable module for managing skills:

# modules/skills/main.tf
variable "skills" {
  type = list(object({
    name        = string
    description = optional(string, "")
  }))
  description = "List of routing skills to create/manage."
}

resource "genesyscloud_routing_skill" "skills" {
  for_each    = { for s in var.skills : s.name => s }
  
  name        = each.value.name
  description = each.value.description
}

output "skill_ids" {
  value = { for k, v in genesyscloud_routing_skill.skills : k => v.id }
}

4. Queue Module

# modules/queues/main.tf
variable "queues" {
  type = list(object({
    name                     = string
    description              = optional(string, "")
    divisionId               = optional(string)
    media_settings_voice_max_wait  = optional(number, 300)  # 5 minutes default
    service_level_percentage       = optional(number, 80)   # 80% SLA target
    service_level_duration_ms      = optional(number, 30000) # 30 seconds
    skill_evaluation_method        = optional(string, "BEST")
    auto_answer_only               = optional(bool, false)
    skill_ids                      = optional(list(string), [])
  }))
}

resource "genesyscloud_routing_queue" "queues" {
  for_each = { for q in var.queues : q.name => q }
  
  name        = each.value.name
  description = each.value.description
  
  media_settings_call {
    alerting_timeout_sec       = 30
    service_level_percentage   = each.value.service_level_percentage / 100
    service_level_duration_ms  = each.value.service_level_duration_ms
  }
  
  skill_evaluation_method = each.value.skill_evaluation_method
  auto_answer_only        = each.value.auto_answer_only
  
  dynamic "routing_rules" {
    for_each = each.value.skill_ids
    content {
      skill_ids_to_remove = [routing_rules.value]
      wait_seconds        = 60
    }
  }
}

output "queue_ids" {
  value = { for k, v in genesyscloud_routing_queue.queues : k => v.id }
}

5. Root Configuration (Environment-Specific)

# environments/production/main.tf
module "skills" {
  source = "../../modules/skills"
  
  skills = [
    { name = "Billing", description = "Billing and payment inquiries" },
    { name = "TechnicalSupport", description = "Technical troubleshooting" },
    { name = "RetentionSpecialist", description = "Customer retention specialists" },
    { name = "Spanish", description = "Spanish language support" },
  ]
}

module "queues" {
  source = "../../modules/queues"
  
  queues = [
    {
      name                        = "Billing_Standard"
      description                 = "Standard billing queue"
      service_level_percentage    = 80
      service_level_duration_ms   = 30000
      skill_ids                   = [module.skills.skill_ids["Billing"]]
    },
    {
      name                        = "Billing_Priority_VIP"
      description                 = "VIP billing escalations"
      service_level_percentage    = 95
      service_level_duration_ms   = 15000
      auto_answer_only            = true
      skill_ids                   = [module.skills.skill_ids["Billing"], module.skills.skill_ids["RetentionSpecialist"]]
    },
    {
      name                     = "TechSupport_General"
      description              = "General technical support"
      service_level_percentage = 75
      service_level_duration_ms = 60000
      skill_ids                = [module.skills.skill_ids["TechnicalSupport"]]
    },
  ]
  
  depends_on = [module.skills]
}

6. The CI/CD Workflow

# .github/workflows/terraform-genesys.yml
on:
  pull_request:
    paths: ['environments/**', 'modules/**']
  push:
    branches: [main]
    paths: ['environments/**', 'modules/**']

jobs:
  plan:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      
      - name: Terraform Plan
        run: |
          terraform init
          terraform plan -out=tfplan
        working-directory: environments/production
        env:
          GENESYS_CLIENT_ID: ${{ secrets.GENESYS_CLIENT_ID }}
          GENESYS_CLIENT_SECRET: ${{ secrets.GENESYS_CLIENT_SECRET }}
      
      - name: Comment Plan on PR
        uses: borchero/terraform-plan-comment@v1
        with:
          plan-file: environments/production/tfplan

  apply:
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      
      - name: Terraform Apply
        run: |
          terraform init
          terraform apply -auto-approve
        working-directory: environments/production
        env:
          GENESYS_CLIENT_ID: ${{ secrets.GENESYS_CLIENT_ID }}
          GENESYS_CLIENT_SECRET: ${{ secrets.GENESYS_CLIENT_SECRET }}

Validation, Edge Cases & Troubleshooting

Edge Case 1: Existing Configuration Not in Terraform State

If you already have 50 queues in Genesys Cloud (created manually) and you start using Terraform, the first terraform apply will attempt to create duplicate queues-not manage the existing ones.
Solution: Use terraform import to import existing resources into the Terraform state before running terraform plan. Most Genesys Cloud Terraform resources support import. Example: terraform import module.queues.genesyscloud_routing_queue.queues["Billing_Standard"] <queue-id>.

Edge Case 2: Queue Deletion Removing Active Interactions

If a Terraform apply deletes a queue that currently has interactions waiting or agents logged in, Genesys Cloud may refuse the deletion or silently strand the interactions.
Solution: Add a lifecycle rule to prevent accidental deletion: lifecycle { prevent_destroy = true }. For intentional deletions, require manual removal of the prevent_destroy flag in a separate PR, giving the team time to verify the queue is empty before the apply.

Edge Case 3: Secret Rotation Breaking State Backend Access

If the AWS credentials used to access the S3 Terraform state backend are rotated, all pipeline runs fail until the secrets are updated in GitHub.
Solution: Use IAM Roles (not IAM user access keys) for the CI pipeline with GitHub Actions OIDC integration. IAM roles auto-rotate, eliminating long-lived credential management.

Official References