Tfmigrate Migration Patterns
Use these patterns when writing tfmigrate files for Atmos Terraform components. Atmos invokes tfmigrate from the
resolved component working directory after init and workspace selection.
Single-State Migrations
Use migration "state" for moves inside one Terraform state.
migration "state" "rename_security_groups" {actions = ["mv aws_security_group.web aws_security_group.app","mv aws_security_group.worker aws_security_group.jobs",]}
Set dir only when the migration needs a working directory different from the command working directory.
migration "state" "rename_from_subdir" {dir = "components/terraform/vpc"actions = ["mv aws_subnet.private aws_subnet.private_primary",]}
Set workspace only when not relying on Atmos workspace selection.
migration "state" "workspace_specific_move" {workspace = env.ATMOS_TERRAFORM_WORKSPACEactions = ["mv aws_route_table.private aws_route_table.private_primary",]}
Common Actions
Rename or move addresses:
migration "state" "move_to_module" {actions = ["mv aws_iam_role.app module.iam.aws_iam_role.app",]}
Remove state bindings without destroying remote infrastructure:
migration "state" "forget_legacy_object" {actions = ["rm aws_s3_bucket_ownership_controls.legacy",]}
Import existing infrastructure into a new address:
migration "state" "import_existing_bucket" {actions = ["import aws_s3_bucket.logs my-company-logs",]}
Replace provider source addresses:
migration "state" "replace_provider" {actions = ["replace-provider registry.terraform.io/-/aws registry.terraform.io/hashicorp/aws",]}
Wildcard moves with xmv:
migration "state" "rename_many_instances" {actions = ["xmv aws_security_group.* aws_security_group.$${1}_primary",]}
for_each and Count Addresses
Quote addresses with embedded string keys so the shell-like action parser preserves them.
migration "state" "count_to_for_each" {actions = ["mv aws_subnet.private[0] 'aws_subnet.private[\"az-a\"]'","mv aws_subnet.private[1] 'aws_subnet.private[\"az-b\"]'",]}
Keep exact source addresses from atmos terraform state list. Do not infer index or key names from Terraform code.
Multi-State Migrations
Use migration "multi_state" when splitting, merging, or moving resources between state files or component
directories.
migration "multi_state" "move_dns_to_dns_component" {from_dir = "components/terraform/network"to_dir = "components/terraform/dns"actions = ["mv aws_route53_zone.primary aws_route53_zone.primary","mv aws_route53_record.app aws_route53_record.app",]}
Use wildcard multi-state moves only after confirming the matched addresses.
migration "multi_state" "move_all_dns_resources" {from_dir = "components/terraform/network"to_dir = "components/terraform/dns"actions = ["xmv aws_route53_*.* $1",]}
Variables: Atmos passes its generated varfile to tfmigrate's internal
convergence-check plans, in both from_dir and to_dir. This works correctly
when both components share the same effective variables. That's the common
case: one component's state relocating. When from_dir and to_dir have
materially different variables, set from_skip_plan = true and
to_skip_plan = true on the migration block instead of relying on the shared
varfile. tfmigrate has no way to apply a different -var-file to each
side's plan.
Output-only diffs: in the usual multi-state pattern, from_dir no longer
declares the resource that moved out. Removing that resource from the config
also removes any output that referenced it. tfmigrate's convergence check
then flags this as an "unexpected diff," even though no resource attribute
changed. Set force = true on the migration block when this is the only
diff. Verify this from the error output: it should show only
Changes to Outputs, and no resource changes. The same gotcha applies to any
migration that changes an output's shape, for example count to for_each.
History Configuration
History mode applies unapplied files from migration_dir in filename order and records applied migrations in durable
storage. Use this mode for hooks and CI.
tfmigrate {migration_dir = "./tfmigrate"history {storage "s3" {bucket = env.ATMOS_TFMIGRATE_HISTORY_BUCKETkey = env.ATMOS_TFMIGRATE_HISTORY_KEYregion = env.ATMOS_TFMIGRATE_HISTORY_REGIONrole_arn = env.ATMOS_TFMIGRATE_HISTORY_ROLE_ARN}}}
For GCS-backed Terraform state, Atmos exposes the bucket value as ATMOS_TFMIGRATE_HISTORY_BUCKET:
tfmigrate {migration_dir = "./tfmigrate"history {storage "gcs" {bucket = env.ATMOS_TFMIGRATE_HISTORY_BUCKETkey = env.ATMOS_TFMIGRATE_HISTORY_KEY}}}
Local history is acceptable only when the file is persisted across reruns:
tfmigrate {migration_dir = "./tfmigrate"history {storage "local" {path = env.ATMOS_TFMIGRATE_HISTORY_PATH}}}
Atmos Hook Examples
Single-file hook:
hooks:state-migration:events:- before.terraform.plan- before.terraform.applykind: tfmigratemigration: 20260527090000_refactor.hclmode: dynamic
History-mode hook:
hooks:state-migration:events:- before.terraform.plan- before.terraform.applykind: tfmigrateconfig: .tfmigrate.hclmode: dynamic
Hook with explicit backend config entries passed to tfmigrate:
hooks:state-migration:events:- before.terraform.plan- before.terraform.applykind: tfmigrateconfig: .tfmigrate.hclbackend_config:- bucket=my-state-bucket- region=us-east-1
Review Checklist
- Migration file has exactly one
migrationblock. - File extension is
.hclor.json. - Filename is sortable when history mode is used.
- All addresses are copied from state output or reviewed Terraform plan output.
plansucceeds beforeapply.- Post-migration Terraform plan does not show unexpected replacement, destroy, or create operations.
- Durable history storage is configured for hooks and CI.