Migrate multi-runner v1 state to v2
The scripts/migrate_multi_runner_state.py utility moves Terraform state
addresses from the multi-runner v1 module layout to the v2
runner_configs layout. It does not create, destroy, or modify AWS
resources. It runs the appropriate state mv commands so Terraform or
OpenTofu continues managing the existing resources after the configuration is
changed to v2.
This procedure is intended for an existing deployment that uses the multi-runner v1 configuration.
Before you start
- Use the migration script from the same repository revision as the v2 module configuration you will deploy.
- Schedule a maintenance window and make sure no other Terraform, OpenTofu, or Terragrunt operation is running against the state.
- Confirm that the v1 configuration is initialized against the production backend and that the backend configuration will remain the same during the migration.
- Confirm that every v1 runner configuration is represented in the current
state. The script discovers dynamic keys from addresses such as
module.runners["large"]; it does not require the keys to be entered on the command line. - Ensure the identity running the command can read and update the state and can acquire the backend lock.
The state backup can contain secrets. Store it in a protected location with
encryption and access controls. The script refuses to overwrite an existing
backup path and writes a newly created backup with mode 0600.
1. Plan the migration
Run the normal v1 plan first and confirm that it is understood and safe:
terraform -chdir="/path/to/terraform-root" init
terraform -chdir="/path/to/terraform-root" plan
Run the migration script without --apply to produce a dry-run mapping:
python3 /path/to/terraform-aws-github-runner/scripts/migrate_multi_runner_state.py \
--working-directory /path/to/terraform-root \
--tool terraform
Review every source -> target pair. The script reports the number of runner
keys, mappings, and generated moves. It exits without changing state unless
--apply is supplied.
For OpenTofu, use --tool tofu and run the equivalent tofu commands. For a
Terragrunt-managed root, use --tool terragrunt and the directory containing
the Terragrunt configuration.
2. Apply the state moves
After reviewing the dry-run output, run the same command with a new backup
path and --apply:
python3 /path/to/terraform-aws-github-runner/scripts/migrate_multi_runner_state.py \
--working-directory /path/to/terraform-root \
--tool terraform \
--backup /path/to/protected-backups/multi-runner-v1-before-state-migration.tfstate \
--apply
Without --yes, the script asks for the exact confirmation word move.
For an already reviewed, non-interactive run, add --yes:
python3 /path/to/terraform-aws-github-runner/scripts/migrate_multi_runner_state.py \
--working-directory /path/to/terraform-root \
--tool terraform \
--backup /path/to/protected-backups/multi-runner-v1-before-state-migration.tfstate \
--apply \
--yes
The backup is taken with <tool> state pull immediately before the first
move. State moves are executed one at a time. If a move fails, the script
stops and reports that migration is incomplete; do not blindly rerun it.
Inspect the state and the backup first.
The script refuses to continue if a destination address already exists. This protects against overwriting an existing v2 state object.
3. Switch the configuration to v2
After the state move completes, update the root module configuration to the v2 contract while keeping the same state backend and root module address. Set the explicit feature flag:
experimental_features = ["multi-runner-v2"]
Use the v2 runner_configs configuration and remove the v1-only configuration
from the root module. Then initialize and plan from the same working
directory:
terraform -chdir="/path/to/terraform-root" init
terraform -chdir="/path/to/terraform-root" plan -detailed-exitcode
The plan should not propose destroying and recreating resources solely because their module addresses changed. Review any remaining changes carefully; state migration does not suppress genuine configuration changes, provider drift, or backend changes. Apply only after the plan is understood:
terraform -chdir="/path/to/terraform-root" apply
Run a second plan and expect exit code 0 for no changes:
terraform -chdir="/path/to/terraform-root" plan -detailed-exitcode
Use the equivalent tofu or terragrunt commands when those tools manage
the deployment.
Warning
A backend configuration change is a separate operation. If initialization
reports that the backend changed, stop and resolve the backend migration
deliberately before running state moves. init -migrate-state does not
replace the v1-to-v2 address migration performed by this script.
Recovery
If the migration stops part-way through or the post-migration plan is not
acceptable, stop further applies and preserve the current state for
investigation. The backup passed to --backup is a snapshot from before the
first move. Restoring it is an operator decision because state push can
replace the current remote state:
terraform -chdir="/path/to/terraform-root" state push \
/path/to/protected-backups/multi-runner-v1-before-state-migration.tfstate
Only restore after confirming the backup is the intended state, no newer
changes must be retained, and the backend is locked. Use tofu state push or
terragrunt state push for those tools. After a restore, return to the v1
configuration before planning again.
Command reference
python3 scripts/migrate_multi_runner_state.py [options]
--working-directory PATH State working directory (default: current directory)
--tool TOOL terragrunt, terraform, or tofu (default: terragrunt)
--backup PATH State pull backup path used before --apply
--apply Execute the generated state moves
--yes Skip the interactive confirmation for --apply