Automate Genesys Cloud Queue Provisioning with Terraform for_each and YAML

Automate Genesys Cloud Queue Provisioning with Terraform for_each and YAML

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.
  • This tutorial uses the Genesys Cloud Terraform Provider (genesyscloud/genesyscloud) and the yamldecode function.
  • The code is written in HashiCorp Configuration Language (HCL) with no external scripting languages required.

Prerequisites

  • Terraform Version: 1.5.0 or later.
  • Genesys Cloud Provider: Version 1.20.0 or later.
  • Genesys Cloud API Client: A Service Account or Client Credentials client ID and secret with the queue:write scope.
  • Environment Variables: GENESYS_CLOUD_REGION (e.g., mypurecloud.com or us-east-1.mypurecloud.ie) and GENESYS_CLOUD_CLIENT_ID/GENESYS_CLOUD_CLIENT_SECRET.
  • Dependencies: No external pip or npm packages are needed. This is pure HCL.

Authentication Setup

Terraform handles authentication via the provider block. You must configure the provider to use your Genesys Cloud credentials. It is critical to use environment variables rather than hardcoding secrets in your .tf files.

Create a main.tf file and define the provider:

terraform {
  required_providers {
    genesyscloud = {
      source  = "genesyscloud/genesyscloud"
      version = ">= 1.20.0"
    }
  }
}

provider "genesyscloud" {
  # These are automatically picked up from environment variables
  # GENESYS_CLOUD_CLIENT_ID
  # GENESYS_CLOUD_CLIENT_SECRET
  # GENESYS_CLOUD_REGION
}

If you do not have environment variables set, you can use a credentials block, but this is discouraged for production:

provider "genesyscloud" {
  client_id     = "your-client-id"
  client_secret = "your-client-secret"
  region        = "mypurecloud.com"
}

Implementation

Step 1: Define the Queue Data in YAML

The for_each meta-argument requires a map or a set. A YAML file is an excellent source for structured data. We will define a list of queue objects. Each object will contain the name, description, and outbound_calling_enabled flag.

Create a file named queues.yaml:

queues:
  - name: "Sales Support - Tier 1"
    description: "Handles initial customer inquiries for sales."
    outbound_calling_enabled: true
    wrap_up_code_required: false
    enable_wait_time: true
    enable_queue_metrics: true
  - name: "Sales Support - Tier 2"
    description: "Escalations for complex sales issues."
    outbound_calling_enabled: true
    wrap_up_code_required: true
    enable_wait_time: true
    enable_queue_metrics: true
  - name: "Billing Inquiries"
    description: "Handles billing and payment questions."
    outbound_calling_enabled: false
    wrap_up_code_required: false
    enable_wait_time: true
    enable_queue_metrics: true

Note that we use a key queues to hold the list. The Terraform yamldecode function will parse this into a map.

Step 2: Parse YAML and Create Resources with for_each

In your main.tf, you will use the file() function to read the YAML content and yamldecode() to convert it into a Terraform-compatible map.

The genesyscloud_routing_queue resource supports for_each. This allows Terraform to manage each queue as an independent resource instance. If you add a new queue to the YAML file later, Terraform will only create that new queue, not destroy and recreate the existing ones.

locals {
  # Read and parse the YAML file
  # yamldecode returns a map. We access the 'queues' key which contains the list.
  # To use for_each on a list, we must convert it to a map.
  # We use the 'name' as the key for the map.
  
  raw_queues = yamldecode(file("${path.module}/queues.yaml")).queues

  # Convert the list of objects into a map of objects keyed by name
  # This is necessary because for_each on a list uses indices, which are unstable if items are added/removed.
  # Using a unique name as the key ensures stability.
  queue_map = { for q in local.raw_queues : q.name => q }
}

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

  name    = each.value.name
  description = each.value.description
  enable_wait_time = each.value.enable_wait_time
  enable_queue_metrics = each.value.enable_queue_metrics
  wrap_up_code_required = each.value.wrap_up_code_required
  
  # Outbound calling settings
  outbound_calling_enabled = each.value.outbound_calling_enabled

  # Default settings for new queues to prevent validation errors
  # The provider requires these to be explicitly set or inherited from a default.
  # Here we set explicit defaults to ensure idempotency.
  max_wait_time_ms = 3600000 # 1 hour
  max_queue_size = 10000
  addressable_name = each.value.name
  
  # Language settings (optional but recommended for production)
  languages {
    language_id = "en-US"
    priority = 1
  }
}

Why use a map instead of a list for for_each?
If you use for_each on a list, Terraform identifies resources by their index (0, 1, 2). If you insert a new item at index 0, the old index 0 becomes index 1. Terraform sees this as a change in identity and may destroy the old resource and create a new one. By using a map with a stable key (like the queue name), Terraform recognizes that “Sales Support - Tier 1” still exists and only updates it if the configuration changes.

Step 3: Handle Dependencies and State

Queues often depend on other resources, such as Wrap-up Codes or Language Settings. In this example, we used a hardcoded language ID "en-US". In a production environment, you should retrieve the language ID dynamically to avoid hardcoding.

Add this lookup to your main.tf:

data "genesyscloud_language" "english" {
  name = "English (United States)"
}

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

  name    = each.value.name
  description = each.value.description
  enable_wait_time = each.value.enable_wait_time
  enable_queue_metrics = each.value.enable_queue_metrics
  wrap_up_code_required = each.value.wrap_up_code_required
  
  outbound_calling_enabled = each.value.outbound_calling_enabled
  max_wait_time_ms = 3600000
  max_queue_size = 10000
  addressable_name = each.value.name
  
  languages {
    language_id = data.genesyscloud_language.english.id
    priority = 1
  }
}

This ensures that even if the language ID changes in Genesys Cloud (rare, but possible in multi-tenant environments), your Terraform state remains consistent.

Complete Working Example

Below is the complete main.tf file. Save this in a directory with the queues.yaml file.

terraform {
  required_providers {
    genesyscloud = {
      source  = "genesyscloud/genesyscloud"
      version = ">= 1.20.0"
    }
  }
}

provider "genesyscloud" {
  # Credentials are expected in environment variables:
  # GENESYS_CLOUD_CLIENT_ID
  # GENESYS_CLOUD_CLIENT_SECRET
  # GENESYS_CLOUD_REGION
}

# Lookup the English language ID to avoid hardcoding
data "genesyscloud_language" "english" {
  name = "English (United States)"
}

locals {
  # Read the YAML file from the current module path
  raw_queues = yamldecode(file("${path.module}/queues.yaml")).queues

  # Convert list to map keyed by name for stable for_each behavior
  queue_map = { for q in local.raw_queues : q.name => q }
}

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

  name    = each.value.name
  description = each.value.description
  
  # Queue behavior settings
  enable_wait_time = each.value.enable_wait_time
  enable_queue_metrics = each.value.enable_queue_metrics
  wrap_up_code_required = each.value.wrap_up_code_required
  
  # Outbound settings
  outbound_calling_enabled = each.value.outbound_calling_enabled
  
  # Capacity and addressability
  max_wait_time_ms = 3600000
  max_queue_size = 10000
  addressable_name = each.value.name
  
  # Language assignment
  languages {
    language_id = data.genesyscloud_language.english.id
    priority = 1
  }
}

# Output the IDs of the created queues for verification
output "queue_ids" {
  value = { for name, queue in genesyscloud_routing_queue.dynamic_queues : name => queue.id }
}

The corresponding queues.yaml file:

queues:
  - name: "Sales Support - Tier 1"
    description: "Handles initial customer inquiries for sales."
    outbound_calling_enabled: true
    wrap_up_code_required: false
    enable_wait_time: true
    enable_queue_metrics: true
  - name: "Sales Support - Tier 2"
    description: "Escalations for complex sales issues."
    outbound_calling_enabled: true
    wrap_up_code_required: true
    enable_wait_time: true
    enable_queue_metrics: true
  - name: "Billing Inquiries"
    description: "Handles billing and payment questions."
    outbound_calling_enabled: false
    wrap_up_code_required: false
    enable_wait_time: true
    enable_queue_metrics: true

To run this:

  1. Initialize the provider:
    terraform init
    
  2. Plan the changes:
    terraform plan
    
  3. Apply the changes:
    terraform apply
    

Common Errors & Debugging

Error: Error: Invalid index

What causes it:
This error occurs if you try to access a key in the YAML map that does not exist. For example, if your YAML file has a typo in enable_wait_time (e.g., enable_wait), and your HCL references each.value.enable_wait_time, Terraform will fail.

How to fix it:
Ensure the keys in your HCL resource block exactly match the keys in your YAML file. Use terraform console to debug the parsed structure:

terraform console
> yamldecode(file("queues.yaml"))

Verify the output matches your expectations.

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

What causes it:
If you manually created a queue in the Genesys Cloud Admin Console with the same name as one in your YAML file, Terraform will detect a conflict. The provider uses the name as a unique identifier for lookup.

How to fix it:
Import the existing queue into Terraform state. First, find the queue ID in Genesys Cloud or via API. Then run:

terraform import genesyscloud_routing_queue.dynamic_queues["Sales Support - Tier 1"] <queue-id>

Replace <queue-id> with the actual ID. Note the syntax: ["Sales Support - Tier 1"] must match the key in your queue_map exactly.

Error: Error: Missing required argument

What causes it:
The Genesys Cloud API requires certain fields for queue creation. If your YAML file omits a required field like name or description, the provider will throw an error.

How to fix it:
Ensure every object in your YAML list has all the fields referenced in the genesyscloud_routing_queue resource block. You can set defaults in HCL if you do not want to specify them in YAML:

description = each.value.description != null ? each.value.description : "Default description"

Error: Error: 409 Conflict

What causes it:
This often happens if you are trying to create a queue with a name that is already in use by another queue in the same organization.

How to fix it:
Check the Genesys Cloud Admin Console for existing queues with the same name. Rename the queue in your YAML file or delete the conflicting queue in the console.

Official References