Automate Genesys Cloud Queue Provisioning with Terraform for_each

Automate Genesys Cloud Queue Provisioning with Terraform for_each

What You Will Build

  • A Terraform configuration that reads a YAML file containing queue definitions and creates multiple Genesys Cloud CX queues in a single apply cycle.
  • This uses the Genesys Cloud Terraform Provider and standard Terraform HCL syntax.
  • The implementation covers Python for YAML parsing and HashiCorp Configuration Language (HCL) for resource provisioning.

Prerequisites

  • Terraform: Version 1.5 or higher installed and initialized.
  • Genesys Cloud Provider: Version 1.40 or higher.
  • Python: Version 3.8+ with the pyyaml package installed (pip install pyyaml).
  • Genesys Cloud Permissions: An OAuth client or user token with the queue:queue:write scope.
  • Directory Structure:
    project/
    ├── main.tf
    ├── variables.tf
    ├── queue_definitions.yaml
    └── parse_yaml.py
    

Authentication Setup

The Genesys Cloud Terraform provider supports multiple authentication methods. For programmatic automation and CI/CD pipelines, the OAuth Client Credentials flow is the standard.

You must configure the provider in main.tf. The provider will handle token acquisition and refresh automatically if you provide the client credentials.

# main.tf

terraform {
  required_providers {
    genesyscloud = {
      source  = "mikesplain/genesyscloud"
      version = "~> 1.40"
    }
  }
}

provider "genesyscloud" {
  # Using environment variables for security
  # Set these in your shell or CI/CD secret store
  # export GENESYCLOUD_CLIENT_ID=your_client_id
  # export GENESYCLOUD_CLIENT_SECRET=your_client_secret
  # export GENESYCLOUD_REGION=us-east-1
}

Required Scope: queue:queue:write

If you are testing locally, you can use a user token, but this is not recommended for production automation due to token expiration limits and security audit trails.

provider "genesyscloud" {
  # Local testing only
  access_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Implementation

Step 1: Define the Queue Data Structure in YAML

We define the queue specifications in a human-readable YAML file. This separates the configuration data from the Terraform logic. This approach allows non-developers (like contact center analysts) to edit queue names and descriptions without touching HCL code.

Create queue_definitions.yaml:

queues:
  - name: "Sales - North America"
    description: "Primary queue for North American sales inquiries"
    enable_wait_time: true
    max_wait_time: 300
    wrap_up_policy: "OPT_IN"
    enabled: true
    address:
      email: "sales-na@example.com"
  - name: "Support - Technical"
    description: "Tier 2 technical support queue"
    enable_wait_time: false
    max_wait_time: 0
    wrap_up_policy: "OPT_IN"
    enabled: true
    address:
      email: "tech-support@example.com"
  - name: "Billing - General"
    description: "General billing questions"
    enable_wait_time: true
    max_wait_time: 60
    wrap_up_policy: "OPT_OUT"
    enabled: true
    address:
      email: "billing@example.com"

Step 2: Parse YAML into Terraform-Compatible JSON

Terraform does not natively parse YAML files into complex nested structures suitable for for_each without external data sources. While Terraform has a file function, it returns a string. We need to convert that string into a map of objects.

The most robust way to do this in a pure Terraform environment is using a local-exec provisioner in a null resource or a pre-processing script. For this tutorial, we will use a Python script to convert the YAML to JSON, which Terraform can read using jsondecode(file("...")).

Create parse_yaml.py:

import yaml
import json
import sys

def convert_yaml_to_json(yaml_file_path, json_file_path):
    try:
        with open(yaml_file_path, 'r') as file:
            data = yaml.safe_load(file)
        
        if 'queues' not in data:
            raise ValueError("YAML file must contain a 'queues' key")
        
        # Transform list to a dictionary keyed by name for for_each compatibility
        # Terraform for_each requires a map/set, not a list
        queues_map = {}
        for queue in data['queues']:
            if 'name' not in queue:
                raise ValueError("Each queue object must have a 'name' field")
            queues_map[queue['name']] = queue
        
        with open(json_file_path, 'w') as file:
            json.dump(queues_map, file, indent=4)
            
        print(f"Successfully converted {yaml_file_path} to {json_file_path}")
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    # Default paths
    yaml_path = "queue_definitions.yaml"
    json_path = "queue_definitions.json"
    
    # Allow command line overrides
    if len(sys.argv) > 2:
        yaml_path = sys.argv[1]
        json_path = sys.argv[2]
        
    convert_yaml_to_json(yaml_path, json_path)

Run this script before applying Terraform:

python parse_yaml.py

This generates queue_definitions.json:

{
    "Sales - North America": {
        "name": "Sales - North America",
        "description": "Primary queue for North American sales inquiries",
        "enable_wait_time": true,
        "max_wait_time": 300,
        "wrap_up_policy": "OPT_IN",
        "enabled": true,
        "address": {
            "email": "sales-na@example.com"
        }
    },
    "Support - Technical": {
        "name": "Support - Technical",
        "description": "Tier 2 technical support queue",
        "enable_wait_time": false,
        "max_wait_time": 0,
        "wrap_up_policy": "OPT_IN",
        "enabled": true,
        "address": {
            "email": "tech-support@example.com"
        }
    },
    "Billing - General": {
        "name": "Billing - General",
        "description": "General billing questions",
        "enable_wait_time": true,
        "max_wait_time": 60,
        "wrap_up_policy": "OPT_OUT",
        "enabled": true,
        "address": {
            "email": "billing@example.com"
        }
    }
}

Step 3: Configure Terraform Variables and Locals

In variables.tf, define the variable that holds the parsed JSON data.

# variables.tf

variable "queue_data_json" {
  description = "Path to the JSON file containing queue definitions"
  type        = string
  default     = "queue_definitions.json"
}

In main.tf, load this data into a local variable. This step converts the JSON file content into a Terraform map structure.

# main.tf (continued)

locals {
  # Read the JSON file and decode it into a map
  # The keys of this map will be the queue names
  queue_definitions = jsondecode(file(var.queue_data_json))
}

Step 4: Create Queues Using for_each

Now we define the genesyscloud_routing_queue resource. We use for_each to iterate over the local.queue_definitions map. This ensures that each queue is created as a distinct resource instance, allowing for independent updates and deletions without affecting other queues.

# main.tf (continued)

resource "genesyscloud_routing_queue" "dynamic_queues" {
  for_each = local.queue_definitions

  name        = each.value.name
  description = each.value.description
  enabled     = each.value.enabled

  # Wrap up policy mapping
  # Genesys API expects specific strings: "OPT_IN", "OPT_OUT", "REQUIRED"
  wrap_up_policy = each.value.wrap_up_policy

  # Wait time configuration
  # Note: enable_wait_time is a boolean in our YAML, but Terraform provider
  # handles the underlying API logic for max_wait_time
  enable_wait_time = each.value.enable_wait_time
  max_wait_time    = each.value.max_wait_time

  # Address configuration
  # The provider expects a block for address
  address {
    email = each.value.address.email
  }
}

Important Note on for_each vs count:
We use for_each because queue names are unique identifiers. If you use count, the index is an integer (0, 1, 2). If you delete the middle queue in a count list, all subsequent indices shift, causing Terraform to destroy and recreate all downstream resources. With for_each, the key (queue name) remains stable, so deleting “Support - Technical” only removes that specific resource.

Step 5: Handling Dependencies and Outputs

If other resources (like skill groups or users) need to reference these queues, you can access them via the genesyscloud_routing_queue.dynamic_queues map.

# main.tf (continued)

output "queue_ids" {
  description = "Map of queue names to their Genesys Cloud IDs"
  value       = { for k, v in genesyscloud_routing_queue.dynamic_queues : k => v.id }
}

output "queue_details" {
  description = "Detailed information for each created queue"
  value = {
    for k, v in genesyscloud_routing_queue.dynamic_queues :
    k => {
      id      = v.id
      name    = v.name
      enabled = v.enabled
    }
  }
}

Complete Working Example

Directory Structure

.
├── main.tf
├── variables.tf
├── queue_definitions.yaml
├── parse_yaml.py
└── .env (for credentials)

1. variables.tf

variable "queue_data_json" {
  description = "Path to the JSON file containing queue definitions"
  type        = string
  default     = "queue_definitions.json"
}

2. main.tf

terraform {
  required_providers {
    genesyscloud = {
      source  = "mikesplain/genesyscloud"
      version = "~> 1.40"
    }
  }
}

provider "genesyscloud" {
  # Ensure GENESYCLOUD_CLIENT_ID, GENESYCLOUD_CLIENT_SECRET, and GENESYCLOUD_REGION are set in environment
}

locals {
  queue_definitions = jsondecode(file(var.queue_data_json))
}

resource "genesyscloud_routing_queue" "dynamic_queues" {
  for_each = local.queue_definitions

  name             = each.value.name
  description      = each.value.description
  enabled          = each.value.enabled
  wrap_up_policy   = each.value.wrap_up_policy
  enable_wait_time = each.value.enable_wait_time
  max_wait_time    = each.value.max_wait_time

  address {
    email = each.value.address.email
  }
}

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

3. queue_definitions.yaml

queues:
  - name: "Sales - North America"
    description: "Primary queue for North American sales inquiries"
    enable_wait_time: true
    max_wait_time: 300
    wrap_up_policy: "OPT_IN"
    enabled: true
    address:
      email: "sales-na@example.com"
  - name: "Support - Technical"
    description: "Tier 2 technical support queue"
    enable_wait_time: false
    max_wait_time: 0
    wrap_up_policy: "OPT_IN"
    enabled: true
    address:
      email: "tech-support@example.com"

4. parse_yaml.py

import yaml
import json
import sys

def convert_yaml_to_json(yaml_file_path, json_file_path):
    try:
        with open(yaml_file_path, 'r') as file:
            data = yaml.safe_load(file)
        
        if 'queues' not in data:
            raise ValueError("YAML file must contain a 'queues' key")
        
        queues_map = {}
        for queue in data['queues']:
            if 'name' not in queue:
                raise ValueError("Each queue object must have a 'name' field")
            queues_map[queue['name']] = queue
        
        with open(json_file_path, 'w') as file:
            json.dump(queues_map, file, indent=4)
            
        print(f"Successfully converted {yaml_file_path} to {json_file_path}")
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    yaml_path = "queue_definitions.yaml"
    json_path = "queue_definitions.json"
    
    if len(sys.argv) > 2:
        yaml_path = sys.argv[1]
        json_path = sys.argv[2]
        
    convert_yaml_to_json(yaml_path, json_path)

Execution Steps

  1. Set environment variables:

    export GENESYCLOUD_CLIENT_ID="your_client_id"
    export GENESYCLOUD_CLIENT_SECRET="your_client_secret"
    export GENESYCLOUD_REGION="us-east-1"
    
  2. Initialize Terraform:

    terraform init
    
  3. Convert YAML to JSON:

    python parse_yaml.py
    
  4. Plan and Apply:

    terraform plan
    terraform apply -auto-approve
    

Common Errors & Debugging

Error: Error: Invalid index

Cause: The JSON file is not a map (dictionary) but a list, or the keys in the map are not valid Terraform identifiers.
Fix: Ensure parse_yaml.py converts the YAML list into a dictionary keyed by the queue name. Terraform for_each requires a map of strings or a set of strings.

Error: Error: queue with name 'X' already exists

Cause: You are trying to create a queue that already exists in Genesys Cloud but is not managed by Terraform.
Fix: Use terraform import to bring the existing queue into state, or delete the queue from Genesys Cloud manually before running apply.

terraform import genesyscloud_routing_queue.dynamic_queues["Sales - North America"] <queue-id>

Error: Error: invalid wrap_up_policy

Cause: The YAML file contains a value not accepted by the Genesys API.
Fix: Valid values are OPT_IN, OPT_OUT, and REQUIRED. Check your YAML file for typos.

Error: Error: failed to parse JSON

Cause: The parse_yaml.py script failed or produced invalid JSON.
Fix: Run python parse_yaml.py manually and check the output. Ensure the YAML file is valid.

Error: 403 Forbidden

Cause: The OAuth client lacks the queue:queue:write scope.
Fix: Update the OAuth client in Genesys Cloud Admin to include the required scope.

Official References