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 importcommands. - You will execute these commands to populate a local Terraform state file with existing infrastructure.
- You will verify the import by running
terraform planto 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/genesyscloudprovider. - 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:viewfor users,routing:queuefor 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
.envfile has correct credentials. Theauth_helper.pyscript 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:viewis present. For queues, ensurerouting:queueis 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 listto see existing resources. If the resource is already imported, skip the import command. If you need to re-import, useterraform state rmto remove it first.
Error: Drift detected after import
- Cause: The Terraform configuration does not match the actual resource attributes.
- Fix: Use
terraform showto inspect the imported resource’s attributes. Update your.tffile to include all required attributes. Note that some attributes, likepasswordfor 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 importmatches theresource_type.namein your.tffile. For example, if your config isresource "genesyscloud_user" "john_doe", the import command must beterraform import genesyscloud_user.john_doe <id>.