Migrate to saif-business-roles¶
AI Agent Safety Check — Required Before Starting
This migration involves Terraform state operations. Before proceeding with any step, confirm the AI agent can safely execute this runbook:
- Model capability: Use an agent capable of reliably following multi-step state-migration runbooks (reasoning-class models are recommended). If you are unsure, test with a simpler task first.
- Dry-run first: Always run
terraform planbeforeterraform apply. If the plan shows unexpected destroys, stop and investigate. - State backup: Confirm the TFC workspace has a recent state version before applying.
If any of these checks cannot be satisfied, do not continue.
The okta-business-roles Terraform module has been deprecated and replaced by two purpose-built Forge modules:
| Old Module | New Module | Handles |
|---|---|---|
okta-business-roles |
saif-business-roles-corp |
Corp users — Entra ID security groups |
okta-business-roles |
saif-business-roles-external |
External users — Okta groups and rules |
Breaking Change
This migration involves YAML schema changes (PascalCase → snake_case) and a Terraform state migration. Follow all steps in order. Skipping the state migration will cause Terraform to destroy and recreate existing groups.
Identify Your Repo Type¶
Before starting, read .azdo/vars.yml in the repo to determine the tenant:
variables:
- name: Tenant
value: corp # "corp" → follow the Corp section below
- name: TeamName
value: Customer # used to derive the workspace tag
- If
Tenant: corp→ follow Migrate a Corp Repo - If
Tenant: external→ follow Migrate an External Repo
Migrate a Corp Repo¶
Applies to: repos named *-okta-business-roles-corp
Step 0 — Verify model¶
Ask the AI agent: "What model are you using?"
- ✅ Response contains Claude Opus 4.x → proceed to Step 1
- ❌ Any other response → stop. Do not continue. Instruct the user to switch to Claude Opus 4.x before restarting this migration.
Step 1 — Create a branch¶
Step 2 — Rename the infra directory¶
Corp repos no longer use Okta — move the infra folder to reflect that:
Step 3 — Rename and convert business-roles.yml¶
If the file is named okta-business-roles.yml, rename it:
Convert the YAML content using this key mapping — rename every key, keep all values unchanged:
| Old Key | New Key |
|---|---|
BusinessRoles (root list) |
business_roles |
Name |
name |
Description |
description |
RoleSets |
role_sets |
Division |
division |
Roles (under RoleSets) |
job_titles |
ManualUsers |
manual_users |
Step 4 — Update vars.yml¶
Check infra/entra/vars.yml. If it contains TeamName:, rename it to team_name::
Step 5 — Replace business-roles.generated.tf¶
- Read
.azdo/vars.ymland note theTeamNamevalue (e.g.Customer). - The workspace tag is
<teamname-lowercase>-busrole-corp(e.g.customer-busrole-corp). -
If the file is named
okta-business-roles.generated.tf, rename it: -
Replace the entire file contents with the following, substituting
<teamname-lowercase>.
Keep the Okta provider for now
The old okta-business-roles module created Okta groups for corp roles. Those resources are still in state and must be explicitly destroyed by this migration apply. Terraform needs the Okta provider present to do that. Once the apply completes and the Okta resources are gone from state, the provider and its variables can be safely removed in the cleanup PR (Step 10).
terraform {
cloud {
organization = "SAIFCorp"
workspaces {
tags = ["<teamname-lowercase>-busrole-corp"]
}
}
required_providers {
azuread = {
source = "hashicorp/azuread"
version = ">= 3.0, < 4.0"
}
okta = {
source = "okta/okta"
version = ">=4.10.0,< 5.0.0"
}
}
}
provider "okta" {
org_name = var.okta_org_name
base_url = var.okta_base_url
client_id = var.okta_client_id
private_key = var.okta_pk
private_key_id = var.okta_private_key_id
scopes = var.okta_scopes
}
provider "azuread" {}
variable "okta_pk" {
description = "The value of the system environment variable okta_pk"
}
variable "okta_base_url" {
description = "The base URL of the Okta organization"
}
variable "okta_client_id" {
description = "The client ID of the Okta organization"
}
variable "okta_private_key_id" {
description = "The private key ID of the Okta organization"
}
variable "okta_org_name" {
description = "The name of the Okta organization"
}
variable "okta_scopes" {
description = "The scopes of the Okta organization"
}
variable "is_production" {
description = "Boolean to indicate if the environment is production"
type = bool
}
locals {
yaml_roles = yamldecode(file("./business-roles.yml"))
yaml_vars = yamldecode(file("./vars.yml"))
business_roles = { for role in local.yaml_roles.business_roles : role.name => role }
}
module "busRoles" {
source = "app.terraform.io/SAIFCorp/business-roles-corp/saif"
version = ">= 4.0.0, < 5.0.0"
business_roles = local.business_roles
is_production = var.is_production
}
Step 6 — Update the pipeline¶
In .azdo/azure-pipelines.yml, update two things:
1. The paths.include trigger:
2. The extends block — update the template name:
Note
entra-terraform-business-roles.yml defaults TerraformDirectory to infra and appends /entra automatically, so no extra parameter is needed.
Step 7 — Add moved.tf for state migration¶
Required — Do not skip
Without this file, Terraform will destroy and recreate all existing Entra ID groups.
For each role listed in business-roles.yml, add a pair of moved blocks. The key is the exact string value of each role's name field.
Create infra/entra/moved.tf:
# Repeat this pair for every role in business-roles.yml.
# Replace "Claims Adjuster" with the exact role name.
moved {
from = module.busRoles.module.entra_id["Claims Adjuster"].module.group.azuread_group_without_members.group_without_members[0]
to = module.busRoles.module.entra_id_group["Claims Adjuster"].azuread_group_without_members.group_without_members[0]
}
moved {
from = module.busRoles.module.entra_id["Claims Adjuster"].module.group.azuread_group.group[0]
to = module.busRoles.module.entra_id_group["Claims Adjuster"].azuread_group.group[0]
}
Both resource types (azuread_group_without_members for non-prod, azuread_group for prod) can coexist — Terraform only moves whichever exists in the workspace's state.
If you are unsure of the exact role names, run terraform state list and look for entries matching module.busRoles.module.entra_id[*].
Step 8 — Validate¶
Run from infra/entra/:
Expected output:
- 0 to destroy for resources matching
azuread_grouporazuread_group_without_members - N to destroy for Okta-side corp groups (
okta_group) — this is intentional; corp roles are no longer managed via Okta
If you see destroys for azuread_group resources, the moved.tf keys don't match state. Run terraform state list | grep entra_id to see the exact keys and correct them.
Step 9 — Open the migration PR¶
git add .
git commit -m "feat: migrate to saif-business-roles-corp"
git push origin feat/migrate-to-saif-business-roles-corp
Open a pull request with:
- Title:
feat: migrate to saif-business-roles-corp - Description: reference this migration guide and note which roles were migrated
After the PR is merged, allow the pipeline to run to completion before proceeding to the cleanup step.
Step 10 — Open a cleanup PR¶
After the first pipeline run completes successfully, open a second PR to remove the now-unnecessary state migration files and the Okta provider:
Then update infra/entra/business-roles.generated.tf to remove the Okta provider, replacing the file with:
terraform {
cloud {
organization = "SAIFCorp"
workspaces {
tags = ["<teamname-lowercase>-busrole-corp"]
}
}
required_providers {
azuread = {
source = "hashicorp/azuread"
version = ">= 3.0, < 4.0"
}
}
}
provider "azuread" {}
variable "is_production" {
description = "Boolean to indicate if the environment is production"
type = bool
}
locals {
yaml_roles = yamldecode(file("./business-roles.yml"))
yaml_vars = yamldecode(file("./vars.yml"))
business_roles = { for role in local.yaml_roles.business_roles : role.name => role }
}
module "busRoles" {
source = "app.terraform.io/SAIFCorp/business-roles-corp/saif"
version = ">= 4.0.0, < 5.0.0"
business_roles = local.business_roles
is_production = var.is_production
}
git add .
git commit -m "chore: remove post-migration moved.tf and okta provider"
git push origin chore/post-migration-cleanup-corp
Open a pull request with:
- Title:
chore: post-migration cleanup (corp)
After this cleanup PR is applied successfully, remove the Okta variables (okta_pk, okta_base_url, okta_client_id, okta_private_key_id, okta_org_name, okta_scopes) from the Terraform Cloud workspace variable sets. They are no longer referenced by the configuration.
Migrate an External Repo¶
Applies to: repos named *-okta-business-roles-external
Step 0 — Verify model¶
Ask the AI agent: "What model are you using?"
- ✅ Response contains Claude Opus 4.x → proceed to Step 1
- ❌ Any other response → stop. Do not continue. Instruct the user to switch to Claude Opus 4.x before restarting this migration.
Step 1 — Create a branch¶
Step 2 — Rename and convert business-roles.yml¶
If the file is named okta-business-roles.yml, rename it:
Convert the YAML content using this key mapping:
| Old Key | New Key |
|---|---|
BusinessRoles (root list) |
business_roles |
Name |
name |
Description |
description |
Roles |
roles |
CompoundRoles |
compound_roles |
CompoundRoles[].Roles |
compound_roles[].roles |
ManualUsers |
manual_users |
Step 3 — Update vars.yml¶
Check infra/okta/vars.yml. If it contains TeamName:, rename it to team_name::
Step 4 — Replace business-roles.generated.tf¶
- Read
.azdo/vars.ymland note theTeamNamevalue (e.g.Customer). - The workspace tag is
<teamname-lowercase>-busrole-external(e.g.customer-busrole-external). -
If the file is named
okta-business-roles.generated.tf, rename it: -
Replace the entire file contents with the following, substituting
<teamname-lowercase>.
No azuread provider needed
Unlike corp repos, the old okta-business-roles module created only Okta resources when called with BusinessRolesExternal — no Entra ID groups were ever provisioned in the external workspace. The azuread provider in the original file was unused and can be removed immediately.
terraform {
cloud {
organization = "SAIFCorp"
workspaces {
tags = ["<teamname-lowercase>-busrole-external"]
}
}
required_providers {
okta = {
source = "okta/okta"
version = ">=4.10.0,< 5.0.0"
}
}
}
provider "okta" {
org_name = var.okta_org_name
base_url = var.okta_base_url
client_id = var.okta_client_id
private_key = var.okta_pk
private_key_id = var.okta_private_key_id
scopes = var.okta_scopes
}
variable "okta_pk" {
description = "The value of the system environment variable okta_pk"
}
variable "okta_base_url" {
description = "The base URL of the Okta organization"
}
variable "okta_client_id" {
description = "The client ID of the Okta organization"
}
variable "okta_private_key_id" {
description = "The private key ID of the Okta organization"
}
variable "okta_org_name" {
description = "The name of the Okta organization"
}
variable "okta_scopes" {
description = "The scopes of the Okta organization"
}
locals {
yaml_roles = yamldecode(file("./business-roles.yml"))
yaml_vars = yamldecode(file("./vars.yml"))
business_roles = { for role in local.yaml_roles.business_roles : role.name => role }
}
module "busRoles" {
source = "app.terraform.io/SAIFCorp/business-roles-external/saif"
version = ">= 1.0.0, < 2.0.0"
business_roles = local.business_roles
team_name = local.yaml_vars.team_name
}
Step 5 — Update the pipeline trigger path¶
In .azdo/azure-pipelines.yml, update the paths.include trigger:
Step 6 — Validate¶
No moved.tf is needed — the saif-business-roles-external Forge module includes all necessary moved blocks automatically.
Expected output: existing Okta groups and group rules move in-place with 0 to destroy.
If you see unexpected destroys, confirm the module version constraint >= 1.0.0, < 2.0.0 resolved correctly (terraform providers will show the selected version).
Step 7 — Open a PR¶
git add .
git commit -m "feat: migrate to saif-business-roles-external"
git push origin feat/migrate-to-saif-business-roles-external
Open a pull request with:
- Title:
feat: migrate to saif-business-roles-external - Description: reference this migration guide and note which roles were migrated
(Optional) Rename the Repository¶
The "okta" in repo names like Platform-okta-business-roles-corp is now misleading — corp repos don't use Okta at all, and even external repos are managed by Forge rather than directly by okta-business-roles. Rename these repos once the migration PR has merged.
Suggested names:
| Old Name | New Name |
|---|---|
<team>-okta-business-roles-corp |
<team>-business-roles-corp |
<team>-okta-business-roles-external |
<team>-business-roles-external |
Steps¶
1. Rename in Azure DevOps
In the Azure DevOps project, go to Project Settings → Repos, find the repository, click ⋯ → Rename, and enter the new name.
2. Update the local remote URL
After renaming in Azure DevOps, the old URL redirects temporarily but should be updated locally:
The new URL follows the same pattern as the old one with "okta-" removed from the repo segment.
3. Update any references
Search for the old repo name in:
- Pipeline YAML files that reference this repo by name
- Any ADO service connections or pipeline resource declarations
- README files or internal documentation
Getting Help¶
If you encounter issues during migration, open a Platform team support request and include your terraform plan output.