Import Existing Genesys Cloud Resources into Terraform State

Import Existing Genesys Cloud Resources into Terraform State

What You Will Build

  • You will write a Python script that queries the Genesys Cloud API to retrieve resource identifiers and constructs the corresponding Terraform terraform import commands.
  • You will execute these commands to populate a local Terraform state file with existing infrastructure.
  • You will verify the import by running terraform plan to ensure zero drift between state and reality.

Prerequisites

  • Genesys Cloud Organization: Access to an organization with existing resources (users, queues, flows).
  • Terraform: Version 1.5+ installed and initialized with the mivenius/genesyscloud provider.
  • Python: Version 3.9+ installed.
  • Dependencies: requests, python-dotenv.
  • Credentials: A Genesys Cloud Service Account with OAuth2 Client Credentials grant type. The account must have sufficient permissions to read the resources you intend to import (e.g., user:view for users, routing:queue for queues).
  • Environment Variables: GENESYS_CLOUD_REGION, GENESYS_CLOUD_CLIENT_ID, GENESYS_CLOUD_CLIENT_SECRET.

Authentication Setup

Terraform requires authentication to interact with the Genesys Cloud API. While Terraform handles its own authentication via the provider configuration, our import helper script needs independent access to fetch resource IDs. We will use the OAuth2 Client Credentials flow.

Create a .env file in your project root:

GENESYS_CLOUD_REGION=mypurecloud.com
GENESYS_CLOUD_CLIENT_ID=your_client_id_here
GENESYS_CLOUD_CLIENT_SECRET=your_client_secret_here

Create a Python module auth_helper.py to handle token retrieval. This script fetches an access token and caches it for the duration of the script run.

import os
import requests
from dotenv import load_dotenv

load_dotenv()

def get_access_token() -> str:
    """
    Retrieves an OAuth2 access token from Genesys Cloud.
    Returns the token string.
    """
    region = os.getenv("GENESYS_CLOUD_REGION")
    client_id = os.getenv("GENESYS_CLOUD_CLIENT_ID")
    client_secret = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")

    if not all([region, client_id, client_secret]):
        raise ValueError("Missing required environment variables: REGION, CLIENT_ID, CLIENT_SECRET")

    token_url = f"https://login.{region}/oauth/token"
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }
    data = {
        "grant_type": "client_credentials",
        "client_id": client_id,
        "client_secret": client_secret
    }

    response = requests.post(token_url, headers=headers, data=data)
    
    if response.status_code != 200:
        raise Exception(f"Failed to get token: {response.status_code} - {response.text}")

    return response.json()["access_token"]

Implementation

Step 1: Define the Terraform Provider Configuration

Before importing, you must initialize a Terraform project. Create a main.tf file. This file defines the provider but does not yet define the resources. The resources will be added to the state file, and then you must create matching configuration blocks in .tf files.

terraform {
  required_providers {
    genesyscloud = {
      source  = "mivenius/genesyscloud"
      version = "~> 1.30.0"
    }
  }
}

provider "genesyscloud" {
  # Authentication is handled via environment variables
  # GENESYS_CLOUD_CLIENT_ID, GENESYS_CLOUD_CLIENT_SECRET, GENESYS_CLOUD_REGION
}

Initialize the working directory:

terraform init

Step 2: Build the Import Helper Script

We will create a script import_resources.py that queries specific Genesys Cloud APIs to find resources and generates the terraform import commands. We will focus on two common resource types: Users and Queues.

The Genesys Cloud Terraform provider uses specific IDs for imports. For users, it is the numeric ID. For queues, it is also the numeric ID.

import os
import sys
import json
import requests
from auth_helper import get_access_token

def fetch_users(token: str, region: str, search_name: str = None) -> list:
    """
    Fetches users from Genesys Cloud.
    If search_name is provided, filters by name.
    """
    base_url = f"https://api.{region}/api/v2/users"
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }
    
    params = {
        "pageSize": 100,
        "pageNumber": 1
    }
    
    if search_name:
        params["name"] = search_name

    all_users = []
    
    while True:
        response = requests.get(base_url, headers=headers, params=params)
        
        if response.status_code == 401:
            raise Exception("Token expired or invalid. Re-authenticate.")
        elif response.status_code != 200:
            raise Exception(f"Failed to fetch users: {response.status_code} - {response.text}")
        
        users = response.json().get("entities", [])
        all_users.extend(users)
        
        if len(users) < 100:
            break
            
        params["pageNumber"] += 1
        
    return all_users

def fetch_queues(token: str, region: str, search_name: str = None) -> list:
    """
    Fetches queues from Genesys Cloud.
    """
    base_url = f"https://api.{region}/api/v2/routing/queues"
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }
    
    params = {
        "pageSize": 100,
        "pageNumber": 1
    }
    
    if search_name:
        params["name"] = search_name

    all_queues = []
    
    while True:
        response = requests.get(base_url, headers=headers, params=params)
        
        if response.status_code == 401:
            raise Exception("Token expired or invalid. Re-authenticate.")
        elif response.status_code != 200:
            raise Exception(f"Failed to fetch queues: {response.status_code} - {response.text}")
        
        queues = response.json().get("entities", [])
        all_queues.extend(queues)
        
        if len(queues) < 100:
            break
            
        params["pageNumber"] += 1
        
    return all_queues

def generate_import_commands(resources: list, resource_type: str, tf_resource_name: str) -> list:
    """
    Generates terraform import commands for a list of resources.
    """
    commands = []
    for res in resources:
        # Sanitize names for Terraform resource naming if using dynamic naming
        # For this example, we assume you will manually create the resource blocks
        # with the exact name matching the ID or a logical name.
        # The import command format is: terraform import <resource_type>.<name> <id>
        
        # We will output a command that imports into a resource named by its ID
        # to avoid naming conflicts during the initial import phase.
        # Later, you can rename the resource blocks in your .tf files.
        
        res_id = res["id"]
        # Format: genesyscloud_user.user_12345
        tf_resource_address = f"{resource_type}.{resource_type}_{res_id}"
        
        cmd = f"terraform import {tf_resource_address} {res_id}"
        commands.append(cmd)
        
    return commands

def main():
    token = get_access_token()
    region = os.getenv("GENESYS_CLOUD_REGION")
    
    # Example: Import all users with "Support" in their name
    search_term = os.getenv("IMPORT_SEARCH_TERM", "")
    
    print(f"Fetching users with name containing '{search_term}'...")
    users = fetch_users(token, region, search_term if search_term else None)
    
    print(f"Fetching queues with name containing '{search_term}'...")
    queues = fetch_queues(token, region, search_term if search_term else None)
    
    print("\n--- Generated Import Commands ---")
    
    user_commands = generate_import_commands(users, "genesyscloud_user", "user")
    for cmd in user_commands:
        print(cmd)
        
    queue_commands = generate_import_commands(queues, "genesyscloud_routing_queue", "queue")
    for cmd in queue_commands:
        print(cmd)
        
    print("\n--- Next Steps ---")
    print("1. Copy the commands above and run them in your terminal.")
    print("2. Create corresponding resource blocks in your .tf files.")
    print("3. Run 'terraform plan' to verify drift.")

if __name__ == "__main__":
    main()

Step 3: Execute the Import

Run the script to generate the commands. Assume you have a user named “John Doe” with ID 1001 and a queue named “Sales Support” with ID 2002.

python import_resources.py

Output:

--- Generated Import Commands ---
terraform import genesyscloud_user.user_1001 1001
terraform import genesyscloud_routing_queue.queue_2002 2002

--- Next Steps ---
1. Copy the commands above and run them in your terminal.
2. Create corresponding resource blocks in your .tf files.
3. Run 'terraform plan' to verify drift.

Execute the generated commands in your terminal:

terraform import genesyscloud_user.user_1001 1001
terraform import genesyscloud_routing_queue.queue_2002 2002

Terraform will output details about the imported resource. For example:

genesyscloud_user.user_1001: Importing from ID "1001"...
genesyscloud_user.user_1001: Import prepared!
  Prepared genesyscloud_user for import
genesyscloud_user.user_1001: Refreshing state... [id=1001]

Import successful!

Step 4: Create Matching Resource Blocks

After importing, the resource exists in the state file (terraform.tfstate), but not in your configuration. You must create a matching resource block in your .tf file. If you do not, terraform plan will show that the resource needs to be destroyed.

Add the following to main.tf. You must match the attributes retrieved from the API. You can use terraform show to view the current state attributes.

terraform show -json > state.json

Inspect state.json for the genesyscloud_user.user_1001 resource. Copy the relevant attributes into your configuration.

resource "genesyscloud_user" "user_1001" {
  name          = "John Doe"
  email         = "john.doe@example.com"
  division_id   = "default" # Or the specific division ID found in state
  username      = "john.doe"
  
  # Note: Password is never returned by the API and cannot be managed via Terraform import.
  # You must exclude the password attribute or use a separate mechanism to set it.
}

resource "genesyscloud_routing_queue" "queue_2002" {
  name        = "Sales Support"
  description = "Queue for sales support inquiries"
  division_id = "default"
  
  # Include other required attributes found in the state file
  # such as wrap_up_policy, queue_flow_id, etc.
}

Step 5: Verify Drift

Run terraform plan. If your configuration matches the state exactly, the output should be:

No changes. Your infrastructure matches the configuration.

If there are differences, Terraform will show them. For example, if the API returned an enabled field that you did not include in your config, Terraform will suggest setting it. Update your .tf file to match the state until the plan shows no changes.

Complete Working Example

Here is the full directory structure and files.

Directory Structure:

genesys-import/
├── .env
├── auth_helper.py
├── import_resources.py
├── main.tf
└── terraform.tfstate (generated after import)

.env

GENESYS_CLOUD_REGION=mypurecloud.com
GENESYS_CLOUD_CLIENT_ID=your_client_id
GENESYS_CLOUD_CLIENT_SECRET=your_client_secret
IMPORT_SEARCH_TERM=Support

auth_helper.py

import os
import requests
from dotenv import load_dotenv

load_dotenv()

def get_access_token() -> str:
    region = os.getenv("GENESYS_CLOUD_REGION")
    client_id = os.getenv("GENESYS_CLOUD_CLIENT_ID")
    client_secret = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")

    if not all([region, client_id, client_secret]):
        raise ValueError("Missing required environment variables: REGION, CLIENT_ID, CLIENT_SECRET")

    token_url = f"https://login.{region}/oauth/token"
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }
    data = {
        "grant_type": "client_credentials",
        "client_id": client_id,
        "client_secret": client_secret
    }

    response = requests.post(token_url, headers=headers, data=data)
    
    if response.status_code != 200:
        raise Exception(f"Failed to get token: {response.status_code} - {response.text}")

    return response.json()["access_token"]

import_resources.py

import os
import json
import requests
from auth_helper import get_access_token

def fetch_users(token: str, region: str, search_name: str = None) -> list:
    base_url = f"https://api.{region}/api/v2/users"
    headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
    params = {"pageSize": 100, "pageNumber": 1}
    if search_name:
        params["name"] = search_name

    all_users = []
    while True:
        response = requests.get(base_url, headers=headers, params=params)
        if response.status_code == 401:
            raise Exception("Token expired.")
        if response.status_code != 200:
            raise Exception(f"Failed: {response.status_code}")
        
        users = response.json().get("entities", [])
        all_users.extend(users)
        if len(users) < 100:
            break
        params["pageNumber"] += 1
    return all_users

def fetch_queues(token: str, region: str, search_name: str = None) -> list:
    base_url = f"https://api.{region}/api/v2/routing/queues"
    headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
    params = {"pageSize": 100, "pageNumber": 1}
    if search_name:
        params["name"] = search_name

    all_queues = []
    while True:
        response = requests.get(base_url, headers=headers, params=params)
        if response.status_code == 401:
            raise Exception("Token expired.")
        if response.status_code != 200:
            raise Exception(f"Failed: {response.status_code}")
        
        queues = response.json().get("entities", [])
        all_queues.extend(queues)
        if len(queues) < 100:
            break
        params["pageNumber"] += 1
    return all_queues

def main():
    token = get_access_token()
    region = os.getenv("GENESYS_CLOUD_REGION")
    search_term = os.getenv("IMPORT_SEARCH_TERM", "")
    
    users = fetch_users(token, region, search_term if search_term else None)
    queues = fetch_queues(token, region, search_term if search_term else None)
    
    print("## Terraform Import Commands ##")
    for user in users:
        print(f'terraform import genesyscloud_user.user_{user["id"]} {user["id"]}')
        
    for queue in queues:
        print(f'terraform import genesyscloud_routing_queue.queue_{queue["id"]} {queue["id"]}')

if __name__ == "__main__":
    main()

main.tf

terraform {
  required_providers {
    genesyscloud = {
      source  = "mivenius/genesyscloud"
      version = "~> 1.30.0"
    }
  }
}

provider "genesyscloud" {}

# Add resource blocks here after importing

Common Errors & Debugging

Error: 401 Unauthorized

  • Cause: The OAuth token is expired or invalid.
  • Fix: Ensure your .env file has correct credentials. The auth_helper.py script fetches a new token on every run. If the error persists, verify the client ID and secret in the Genesys Cloud Admin Console.

Error: 403 Forbidden

  • Cause: The service account does not have permission to read the resource.
  • Fix: Check the OAuth scopes assigned to the service account. For users, ensure user:view is present. For queues, ensure routing:queue is present.

Error: Resource already exists

  • Cause: You are trying to import a resource that is already in the Terraform state file.
  • Fix: Run terraform state list to see existing resources. If the resource is already imported, skip the import command. If you need to re-import, use terraform state rm to remove it first.

Error: Drift detected after import

  • Cause: The Terraform configuration does not match the actual resource attributes.
  • Fix: Use terraform show to inspect the imported resource’s attributes. Update your .tf file to include all required attributes. Note that some attributes, like password for users, cannot be imported and must be excluded from the configuration or managed separately.

Error: Invalid resource address

  • Cause: The resource address in the import command does not match the configuration.
  • Fix: Ensure the resource address in terraform import matches the resource_type.name in your .tf file. For example, if your config is resource "genesyscloud_user" "john_doe", the import command must be terraform import genesyscloud_user.john_doe <id>.

Official References