Migrate Genesys Cloud User Resources in Terraform v1.35.0
What You Will Build
- One sentence: This tutorial demonstrates how to refactor Terraform configurations to comply with the schema changes in the
genesyscloud_userresource introduced in provider version 1.35.0. - One sentence: This uses the Genesys Cloud Terraform Provider and the underlying Genesys Cloud REST API (
/api/v2/users) to validate state migration. - One sentence: The implementation covers Terraform HCL configuration, state file manipulation, and Python-based verification scripts.
Prerequisites
- OAuth Client Type: Confidential Client (Client Credentials Grant) or Public Client (Authorization Code Grant) with offline access.
- Required Scopes:
user:read,user:write,user:division:read. - SDK/API Version: Genesys Cloud Terraform Provider v1.35.0+, Genesys Cloud API v2.
- Language/Runtime Requirements: Python 3.9+ (for verification script), Terraform 1.5+.
- External Dependencies:
terraform,python,requests,json.
Authentication Setup
To interact with the Genesys Cloud API for verification or debugging, you must first obtain an OAuth access token. The Terraform provider handles this internally using credentials stored in environment variables or a credentials file, but manual verification requires explicit token retrieval.
OAuth Flow: Client Credentials Grant
import requests
import os
import json
def get_genesys_token(client_id: str, client_secret: str, env_url: str) -> str:
"""
Retrieves an OAuth2 access token from Genesys Cloud.
Args:
client_id: The OAuth client ID.
client_secret: The OAuth client secret.
env_url: The base URL for the environment (e.g., https://api.mypurecloud.com).
Returns:
A valid JWT access token string.
"""
auth_url = f"{env_url}/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret
}
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
try:
response = requests.post(auth_url, data=payload, headers=headers)
response.raise_for_status()
token_data = response.json()
return token_data["access_token"]
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
raise ValueError("Authentication failed: Invalid client ID or secret.") from e
elif response.status_code == 429:
raise RuntimeError("Rate limited: Too many requests. Implement retry logic.") from e
else:
raise RuntimeError(f"HTTP Error: {response.status_code} - {response.text}") from e
except Exception as e:
raise RuntimeError(f"Failed to retrieve token: {str(e)}") from e
# Example Usage
# ENV_URL = os.getenv("GENESYS_ENV_URL", "https://api.mypurecloud.com")
# CLIENT_ID = os.getenv("GENESYS_CLIENT_ID")
# CLIENT_SECRET = os.getenv("GENESYS_CLIENT_SECRET")
# token = get_genesys_token(CLIENT_ID, CLIENT_SECRET, ENV_URL)
Scope Requirement: The token must include the user:write scope to create or update users. If using a confidential client, ensure the client has the necessary permissions in the Genesys Cloud Admin Console under Security > OAuth Clients.
Implementation
Step 1: Identify the Schema Breaking Change
In version 1.35.0 of the genesyscloud Terraform provider, the genesyscloud_user resource underwent a significant schema change regarding how email addresses and phone numbers are handled. Previously, these were often top-level attributes or handled through a less structured phone_numbers block. The new schema aligns more closely with the REST API structure, introducing distinct blocks for phone_numbers and enforcing stricter validation on email.
The Key Change:
The phone_numbers attribute is now a structured block list rather than a simple string or map. Additionally, the email attribute validation has been tightened, and the division_id behavior has been clarified for multi-division tenants.
If you attempt to apply an old configuration with v1.35.0, Terraform will report a plan error indicating that the attribute structure does not match the schema.
Step 2: Refactor the Terraform Configuration
You must update your main.tf or user-specific module to reflect the new block structure.
Old Schema (Pre-v1.35.0 - Deprecated):
# THIS WILL FAIL IN v1.35.0
resource "genesyscloud_user" "agent" {
name = "John Doe"
email = "john.doe@example.com"
division_id = "default"
phone_numbers = "+15550199" # Simplified format, no longer supported as a single string
}
New Schema (v1.35.0+ - Required):
resource "genesyscloud_user" "agent" {
name = "John Doe"
email = "john.doe@example.com"
division_id = "default"
# New structured block for phone numbers
phone_numbers {
number = "+15550199"
type = "WORK" # Valid types: WORK, MOBILE, HOME, OTHER
}
}
Non-Obvious Parameters:
type: In thephone_numbersblock, thetypefield is now mandatory in many contexts to distinguish between work lines and mobile numbers. If omitted, the provider may default toWORK, but explicit declaration prevents drift.division_id: If your tenant uses multiple divisions, you must specify the correctdivision_id. Using"default"works for the default division, but for custom divisions, you must query thegenesyscloud_routing_divisiondata source.
Step 3: Handle State Migration
If you have existing users managed by Terraform, you cannot simply change the code and run terraform apply. Terraform will detect that the schema has changed and may attempt to destroy and recreate the user, which is dangerous for production agents.
You must migrate the state.
Option A: Terraform State Move (Recommended for small changes)
If the attribute name changed but the resource ID remains the same, you might need to use terraform state mv if the resource block name changed. However, for schema attribute changes, Terraform usually handles the state update automatically if you run terraform apply with the new configuration.
Option B: Manual State Edit (For complex migrations)
If Terraform insists on recreating the resource, you must edit the state file to match the new schema.
- Export the state:
terraform state pull > state.json
- Edit the
state.jsonfile. Locate theattributessection for thegenesyscloud_userresource. Update thephone_numbersattribute from a string to a list of objects.
Old State Structure:
"phone_numbers": "+15550199"
New State Structure:
"phone_numbers": [
{
"number": "+15550199",
"type": "WORK"
}
]
- Push the state back:
terraform state push state.json
- Run
terraform planto verify no changes are detected.
Step 4: Verify with Python Script
After migrating the Terraform state and configuration, verify that the user exists in Genesys Cloud with the correct attributes using the REST API.
import requests
import json
import sys
def verify_user(env_url: str, token: str, user_email: str) -> dict:
"""
Verifies the user details in Genesys Cloud via REST API.
Args:
env_url: Base URL (e.g., https://api.mypurecloud.com)
token: OAuth access token
user_email: Email of the user to verify
Returns:
Dictionary containing user details
"""
# Step 1: Search for the user by email
search_url = f"{env_url}/api/v2/users/search"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
payload = {
"query": user_email,
"pageSize": 100
}
try:
response = requests.post(search_url, json=payload, headers=headers)
response.raise_for_status()
users = response.json().get("users", [])
if not users:
print(f"Error: User with email {user_email} not found.")
sys.exit(1)
# Assume the first match is the correct user
user_id = users[0]["id"]
print(f"Found User ID: {user_id}")
# Step 2: Get full user details
user_detail_url = f"{env_url}/api/v2/users/{user_id}"
detail_response = requests.get(user_detail_url, headers=headers)
detail_response.raise_for_status()
user_data = detail_response.json()
# Step 3: Validate phone numbers structure
phone_numbers = user_data.get("phoneNumbers", [])
if not phone_numbers:
print("Warning: No phone numbers found for user.")
else:
print(f"Phone Numbers: {json.dumps(phone_numbers, indent=2)}")
return user_data
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
print("Error: Unauthorized. Check token validity.")
elif response.status_code == 404:
print("Error: User not found.")
else:
print(f"HTTP Error: {e}")
sys.exit(1)
except Exception as e:
print(f"Unexpected error: {e}")
sys.exit(1)
# Example Usage
# ENV_URL = os.getenv("GENESYS_ENV_URL", "https://api.mypurecloud.com")
# TOKEN = get_genesys_token(CLIENT_ID, CLIENT_SECRET, ENV_URL)
# user_details = verify_user(ENV_URL, TOKEN, "john.doe@example.com")
Complete Working Example
Below is a complete Terraform configuration and a Python verification script that you can run together.
Terraform Configuration (main.tf)
terraform {
required_providers {
genesyscloud = {
source = "mikesplain/genesyscloud"
version = ">= 1.35.0"
}
}
}
# Provide credentials via environment variables
provider "genesyscloud" {
# Use environment variables: GENESYS_CLIENT_ID, GENESYS_CLIENT_SECRET, GENESYS_ENV_URL
}
# Data source to get the default division ID
data "genesyscloud_routing_division" "default" {
name = "Default"
}
resource "genesyscloud_user" "example_agent" {
name = "Terraform Test Agent"
email = "terraform.test@example.com"
division_id = data.genesyscloud_routing_division.default.id
# New v1.35.0 Schema
phone_numbers {
number = "+15550199"
type = "WORK"
}
# Optional: Add skills if needed
# skills {
# id = genesyscloud_routing_skill.example.id
# }
}
output "user_id" {
value = genesyscloud_user.example_agent.id
description = "The ID of the created user"
}
Python Verification Script (verify_user.py)
import requests
import os
import json
import sys
def get_token():
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
env_url = os.getenv("GENESYS_ENV_URL", "https://api.mypurecloud.com")
if not client_id or not client_secret:
raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set")
auth_url = f"{env_url}/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret
}
response = requests.post(auth_url, data=payload)
response.raise_for_status()
return response.json()["access_token"]
def verify_user():
token = get_token()
env_url = os.getenv("GENESYS_ENV_URL", "https://api.mypurecloud.com")
user_email = "terraform.test@example.com"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# Search for user
search_url = f"{env_url}/api/v2/users/search"
payload = {"query": user_email, "pageSize": 100}
response = requests.post(search_url, json=payload, headers=headers)
response.raise_for_status()
users = response.json().get("users", [])
if not users:
print(f"User {user_email} not found.")
sys.exit(1)
user_id = users[0]["id"]
# Get details
detail_url = f"{env_url}/api/v2/users/{user_id}"
detail_response = requests.get(detail_url, headers=headers)
detail_response.raise_for_status()
user_data = detail_response.json()
print("User Verification Result:")
print(json.dumps(user_data, indent=2))
# Validate phone numbers
phone_numbers = user_data.get("phoneNumbers", [])
if len(phone_numbers) > 0:
print("Phone numbers correctly structured as list of objects.")
else:
print("Warning: No phone numbers found.")
if __name__ == "__main__":
verify_user()
Common Errors & Debugging
Error: Error: expected phone_numbers to be a list of objects, got string
What causes it:
You are using the old schema where phone_numbers was a single string or map. The provider v1.35.0 expects a block list.
How to fix it:
Update your Terraform configuration to use the phone_numbers block structure as shown in Step 2.
Code showing the fix:
# Incorrect
phone_numbers = "+15550199"
# Correct
phone_numbers {
number = "+15550199"
type = "WORK"
}
Error: Error: 429 Too Many Requests
What causes it:
You are hitting the Genesys Cloud API rate limit. This often happens during large-scale user migrations when Terraform tries to create or update many users in parallel.
How to fix it:
Implement retry logic in your Terraform provider configuration or reduce the concurrency.
Code showing the fix:
provider "genesyscloud" {
# Retry on 429 errors
retry_max_attempts = 5
retry_sleep_seconds = 5
}
Error: Error: 409 Conflict
What causes it:
You are trying to create a user with an email address that already exists in Genesys Cloud.
How to fix it:
Ensure the email address is unique. If the user already exists, use terraform import to import the existing user into your state file instead of creating a new one.
Code showing the fix:
terraform import genesyscloud_user.example_agent <user_id>