Skip to content

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:

  1. 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.
  2. Dry-run first: Always run terraform plan before terraform apply. If the plan shows unexpected destroys, stop and investigate.
  3. 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

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

git checkout -b feat/migrate-to-saif-business-roles-corp

Step 2 — Rename the infra directory

Corp repos no longer use Okta — move the infra folder to reflect that:

git mv infra/okta infra/entra

Step 3 — Rename and convert business-roles.yml

If the file is named okta-business-roles.yml, rename it:

git mv infra/entra/okta-business-roles.yml infra/entra/business-roles.yml

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
BusinessRoles:
  - Name: "Claims Adjuster"
    Description: "Claims staff"
    RoleSets:
      - Division: Claims Division
        Roles:
          - Claims Adjuster I
    ManualUsers: []
business_roles:
  - name: "Claims Adjuster"
    description: "Claims staff"
    role_sets:
      - division: Claims Division
        job_titles:
          - Claims Adjuster I
    manual_users: []

Step 4 — Update vars.yml

Check infra/entra/vars.yml. If it contains TeamName:, rename it to team_name::

TeamName: "Platform"
team_name: "Platform"

Step 5 — Replace business-roles.generated.tf

  1. Read .azdo/vars.yml and note the TeamName value (e.g. Customer).
  2. The workspace tag is <teamname-lowercase>-busrole-corp (e.g. customer-busrole-corp).
  3. If the file is named okta-business-roles.generated.tf, rename it:

    git mv infra/entra/okta-business-roles.generated.tf infra/entra/business-roles.generated.tf
    
  4. 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:

  paths:
    include:
      - infra/okta/okta-business-roles.yml
  paths:
    include:
      - infra/entra/business-roles.yml

2. The extends block — update the template name:

extends:
  template: okta-terraform-business-roles.yml@templates
  parameters:
    Tenant: ${{variables.Tenant}}
    TeamName: ${{variables.TeamName}}
extends:
  template: entra-terraform-business-roles.yml@templates
  parameters:
    Tenant: ${{variables.Tenant}}
    TeamName: ${{variables.TeamName}}

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/:

terraform init -upgrade
terraform plan

Expected output:

  • 0 to destroy for resources matching azuread_group or azuread_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:

git checkout -b chore/post-migration-cleanup-corp
git rm infra/entra/moved.tf

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

git checkout -b feat/migrate-to-saif-business-roles-external

Step 2 — Rename and convert business-roles.yml

If the file is named okta-business-roles.yml, rename it:

git mv infra/okta/okta-business-roles.yml infra/okta/business-roles.yml

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
BusinessRoles:
  - Name: Injured Worker
    Description: Workers who have filed injury claims
    Roles:
      - InjuredWorkerAccess
    CompoundRoles: []
    ManualUsers: []
business_roles:
  - name: Injured Worker
    description: Workers who have filed injury claims
    roles:
      - InjuredWorkerAccess
    compound_roles: []
    manual_users: []

Step 3 — Update vars.yml

Check infra/okta/vars.yml. If it contains TeamName:, rename it to team_name::

TeamName: "Customer"
team_name: "Customer"

Step 4 — Replace business-roles.generated.tf

  1. Read .azdo/vars.yml and note the TeamName value (e.g. Customer).
  2. The workspace tag is <teamname-lowercase>-busrole-external (e.g. customer-busrole-external).
  3. If the file is named okta-business-roles.generated.tf, rename it:

    git mv infra/okta/okta-business-roles.generated.tf infra/okta/business-roles.generated.tf
    
  4. 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:

  paths:
    include:
      - infra/okta/okta-business-roles.yml
  paths:
    include:
      - infra/okta/business-roles.yml

Step 6 — Validate

No moved.tf is needed — the saif-business-roles-external Forge module includes all necessary moved blocks automatically.

terraform init -upgrade
terraform plan

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:

git remote set-url origin <new-clone-url>

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.