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
pyyamlpackage installed (pip install pyyaml). - Genesys Cloud Permissions: An OAuth client or user token with the
queue:queue:writescope. - 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
-
Set environment variables:
export GENESYCLOUD_CLIENT_ID="your_client_id" export GENESYCLOUD_CLIENT_SECRET="your_client_secret" export GENESYCLOUD_REGION="us-east-1" -
Initialize Terraform:
terraform init -
Convert YAML to JSON:
python parse_yaml.py -
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.