Moving and Renaming Accounts
Moving an account to another OU or renaming it changes the key under which NTC Account Factory stores its baseline. Without further action the new baseline pipeline starts with an empty state and tries to create resources which already exist in the account.
account_baseline_state_migrations carries the baseline state over to the new key. Existing resources stay in place and the first run only applies the difference between the old and the new baseline.
Requires NTC Account Factory 2.8.0 or later.
Background
Every combination of baseline scope and account has its own package, pipeline, Terraform state and DynamoDB lock table. They are identified by the instance key <scope_name>/<account_name>:
baseline_scope__workloads-dev/aws-example-workload/baseline.zip
baseline_scope__workloads-dev/aws-example-workload/terraform.tfstate
baseline_scope__workloads-dev__aws-example-workload__baseline.lock (DynamoDB)
The factory manages the package, the pipeline and the lock table. The state is managed by the pipeline only, and the pipeline enables deletion protection on the lock table as long as the state contains resources.
The instance key changes when:
- the account moves into another baseline scope, e.g. a new
ou_pathmatchesinclude_accounts_by_ou_pathsof a different scope (scope part of the key) - the account is renamed (account part of the key)
In both cases the factory removes the old combination and creates a new one. If nothing is declared, two things fail: the factory apply stops on the deletion protection of the old lock table, and the new pipeline runs against an empty state.
Moving an Account to Another Scope
Example: aws-example-workload moves from /root/workloads/dev (scope workloads-dev) to /root/workloads/prod (scope workloads-prod).
Step 1: Move the Account and Declare the Migration
Change ou_path and add the migration entry in the same commit:
{
"account_name": "aws-example-workload",
"ou_path": "/root/workloads/prod"
}
account_baseline_state_migrations = [
{
from = "workloads-dev/aws-example-workload"
to = "workloads-prod/aws-example-workload"
},
]
from and to are the instance keys shown in the plan. Copy them from the baseline resources the plan destroys and creates.
The plan of the Account Factory shows:
aws_organizations_account.ntc_factory_account["aws-example-workload"]: updated in place (new parent OU)workloads-dev/aws-example-workload: package, pipeline and event rules destroyed. The lock table is kept because it is named infrom, so its deletion protection does not fail the applyworkloads-prod/aws-example-workload: package, pipeline, event rules and lock table created
Apply. The new pipeline starts right away.
What the First Run Does
Before terraform init, the new pipeline:
- Checks that its own state does not exist yet or contains no resources
- Checks that the source state exists and contains resources
- Waits until no run of the old baseline is in progress (bounded by the pipeline delay options)
- Copies the source state to its own key and verifies the copy with a SHA-256 checksum
- Renames the source state to
terraform.tfstate.migrated-<timestamp>-to-<new_key> - Disables deletion protection on the old lock table
Then the regular run continues: terraform init picks up the migrated state and terraform apply applies the delta between the two baselines.
The step is logged under [STATE MIGRATION] in the CodeBuild logs (codepipeline mode) or the ECS task logs (ecs mode):
[STATE MIGRATION] Check for baseline state migration
[info] state migration declared from 'workloads-dev/aws-example-workload' to 'workloads-prod/aws-example-workload'
[info] source state 'baseline_scope__workloads-dev/aws-example-workload/terraform.tfstate' found with 42 resources
[info] no run of the previous baseline 'workloads-dev/aws-example-workload' in progress
[info] copying source state to 'baseline_scope__workloads-prod/aws-example-workload/terraform.tfstate'
[info] copied state verified (sha256 ...)
[info] source state renamed to '...terraform.tfstate.migrated-20260917T101500Z-to-workloads-prod__aws-example-workload'
[info] deletion protection DISABLED on lock table 'baseline_scope__workloads-dev__aws-example-workload__baseline.lock'
[success] baseline state migrated from 'workloads-dev/aws-example-workload' to 'workloads-prod/aws-example-workload'
The first run applies the difference between the two baselines. Resources which are only defined in the old scope are destroyed, resources which are only defined in the new scope are created. Compare the templates of both scopes before the move.
To review the delta without applying it, set baseline_maintenance_plan_only = true on the new scope. The state is migrated anyway, and nothing is applied until the flag is removed.
Step 2: Clean Up
After the first run of the new pipeline succeeded, remove the entry and apply again:
account_baseline_state_migrations = []
The plan shows the old lock table being destroyed, since the pipeline has released its deletion protection. The package of the new key is updated as well, which triggers one more regular run.
If destroying the old lock table fails with a deletion protection error, the migration did not run. Check the log of the new pipeline before retrying. Do not disable the deletion protection manually unless you have verified where the state is.
Renaming an Account
The account name is the key of every factory resource of the account: the aws_organizations_account resource and every scope/account combination the account is part of. A rename therefore needs a moved block for the account and one migration entry per scope.
Example: aws-example-wl is renamed to aws-example-workload. The account is in the scopes workloads-dev and security.
Terraform cannot rename an AWS account in place. If account_name or account_email in the configuration differ from what AWS Organizations shows, Terraform plans a replacement of the account resource, which creates a new AWS account. Change the name (and the email, if it changes) in the AWS console first, then update the configuration to match it exactly.
Before You Rename
- Baseline resources which use the account name: Search your baseline templates for
current_account_name. Every resource which uses it in a name, a bucket name or a tag changes in the first run, and resources with immutable names are recreated. - Dependencies outside the Account Factory: Account repositories, Spacelift stacks or other stacks may be keyed by the account name as well. Migrate them together with the rename, otherwise they are destroyed and recreated under the new name.
Step 1: Rename and Declare
- Rename the account in the AWS console (Organizations, account settings)
- Update
account_name(andaccount_email) inaccount_factory_listto match the console exactly - Add a
movedblock and one migration entry per scope in the same commit
moved {
from = module.ntc_account_factory.aws_organizations_account.ntc_factory_account["aws-example-wl"]
to = module.ntc_account_factory.aws_organizations_account.ntc_factory_account["aws-example-workload"]
}
account_baseline_state_migrations = [
{ from = "workloads-dev/aws-example-wl", to = "workloads-dev/aws-example-workload" },
{ from = "workloads-prod/aws-example-wl", to = "workloads-prod/aws-example-workload" },
]
The moved block belongs in the root module which calls the Account Factory. Replace ntc_account_factory with the name of your module call.
Run a plan. The account resource must show a move only. If the plan shows must be replaced or an update of name or email, stop: the console rename is missing or does not match the configuration.
Alternate contacts are keyed by the account name too. They are recreated under the new key, which is a harmless re-put for this API, so no moved block is needed.
Apply. Each new pipeline migrates its own state. Review the first run of every scope.
Step 2: Clean Up
Remove all migration entries and apply. The old lock tables are destroyed. The moved block can be removed after the rename was applied, or kept as documentation of the rename.
Rename and Move at Once
A single entry like { from = "workloads-dev/aws-example-wl", to = "workloads-prod/aws-example-workload" } is valid. The first run then contains both the delta between the scopes and the name-related changes, which is harder to review. Prefer two separate changes: rename first, move second.
Migration Guard
With the guard enabled, a baseline run which would start with an empty state first looks for lock tables tagged with the same AWS account ID. If one of them points to a non-empty state, the run fails before terraform init and prints the missing entry:
account_baseline_state_migration_guard_enabled = true
[error] this baseline run would start with an empty state, but a non-empty baseline state of account id 123456789012 exists under another instance key:
[error] key 'workloads-dev/aws-example-workload' with 42 resources
[error] add the following entries to 'account_baseline_state_migrations' in the NTC Account Factory, or decommission the account in the source scope first:
[error] { from = "workloads-dev/aws-example-workload", to = "workloads-prod/aws-example-workload" }
The guard matches on the account ID, so it catches renames as well. It is disabled by default.
The guard stops the pipeline, not the factory apply: if the entry was forgotten, the factory apply still fails on the deletion protection of the old lock table. Add the entry and apply again. The old lock table was not destroyed, so the retry follows the regular flow.
Edge Cases
- Account in several scopes: Only the scope/account combinations named in
account_baseline_state_migrationsare migrated. The other scopes of the account keep their state and pipeline. - Moving back later: Declare the reverse entry, e.g.
{ from = "workloads-prod/aws-example-workload", to = "workloads-dev/aws-example-workload" }. This works because the first migration renamed the old state. - Old scope already decommissioned: The source state contains no resources. The pipeline logs a warning, releases the old lock table and continues with an empty state. No entry is needed, but adding one does no harm.
- Old pipeline still running: The new pipeline waits for the run to finish. If the run does not finish within the configured delay budget, the new pipeline fails and can be retried.
- Pipeline rerun after the migration: The rerun finds its own state with resources and skips the migration.
- Deployment modes and binaries:
codepipelineandecsmode as well as Terraform and OpenTofu behave the same.
The Account Factory validates the entries at plan time: from and to must be instance keys in the format <scope_name>/<account_name>, must differ, and each key may only appear once as from and once as to. A from scope which does not exist in account_baseline_scopes is reported as a check warning.
Checklist
Before the move or rename
- Compare the templates of the old and the new scope and know which resources the delta destroys
- Rename: rename the account (and email) in the AWS console
- Rename: check the baseline templates for
current_account_nameand the stacks outside the factory which are keyed by the account name
Change
- Change
ou_pathoraccount_nameinaccount_factory_list - Add one entry per affected scope to
account_baseline_state_migrations(and themovedblock for a rename) in the same commit - Apply and confirm the apply succeeded
- Check the
[STATE MIGRATION]log lines of every new pipeline and review the plan of the first run
Cleanup
- Remove the entries from
account_baseline_state_migrations - Apply and confirm the old lock tables were destroyed
Without State Migration
If the baseline resources do not need to survive, decommission the account in the old scope first:
- Add the account to
decommission_accounts_by_namesof the old scope and apply. The pipeline destroys all baseline resources of this scope and releases the lock table. - Wait until the decommission run succeeded.
- Move or rename the account, remove the decommission entry and apply. The new pipeline creates the baseline from scratch.
This recreates every baseline resource (new ARNs, new KMS keys, new bucket names with random suffixes) and leaves the account without baseline controls between step 1 and step 3. Prefer account_baseline_state_migrations unless a clean rebuild is intended.