Skip to main content
RunBook Academy

TerraformX · State Operations: Read, Move, Remove, ImportProduction Terraform

state mv: Renaming Without Recreation

Intermediate⏱ ~12 minbash

What you'll learn

  • Use terraform state mv to rename a resource address in state without touching real infrastructure
  • Choose between state mv (imperative) and a moved block (declarative)
  • Back up state before any mv operation and verify the plan is empty afterwards
  • Recognise when mv is the wrong tool (cross-module, cross-type)

Prerequisites

None — start here.

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13

Not yet marked complete on this device.

A configuration refactor renames aws_instance.web to aws_instance.app. Without intervention, the next plan will destroy web and create app — destroying the live instance and provisioning a new one. The instance has the same AMI, the same size, the same data on disk. From the cloud’s perspective, it is the wrong outcome. From Terraform’s perspective, the address changed; the resource must be replaced.

terraform state mv is the imperative fix: it tells Terraform “the real-world object that lived at address X now lives at address Y”. The cloud is untouched. The next plan sees no changes.

When state mv is the right tool

state mv renames a resource address in state. The real-world object is unaffected. The use cases:

  • Rename a resource in configuration (without a moved block).
  • Re-key a for_each resource to a stable identifier.
  • Move a resource between modules (with the destination block pre-existing in configuration).
  • Recover from a state rm performed by mistake.

The use cases where state mv is the wrong tool:

  • Cross-provider migrations (use state replace-provider).
  • Adopting an existing real-world resource (use import).
  • Permanent refactors that should be reviewed (use a moved block instead — the declarative form is reviewable in code).

The procedure

# 1. Pull state to a backup file
terraform state pull > /tmp/state-before-mv.json

# 2. Confirm source and destination addresses exist where expected
terraform state list | grep 'aws_instance.web'
# aws_instance.web
terraform state list | grep 'aws_instance.app'
# (empty - the destination address has no state yet)

# 3. Confirm the destination configuration exists
grep -A 1 '^resource "aws_instance" "app"' main.tf
# resource "aws_instance" "app" {
#   ...

# 4. Run the mv
terraform state mv -backup-file=/tmp/mv-backup.tfstate \
  'aws_instance.web' 'aws_instance.app'

# 5. Confirm the mv with state list
terraform state list | grep -E 'aws_instance.(web|app)'
# aws_instance.app

# 6. Confirm the plan is empty
terraform plan
# No changes. Your infrastructure matches the configuration.

The -backup-file flag writes the pre-mv state to a file. This is the recovery artifact if the mv has unexpected consequences.

Anatomy of the command

terraform state mv [options] SOURCE DESTINATION

Flags:

FlagPurpose
-dry-runReport the instances the source address matches, without moving any of them.
-backup-file=pathWrite the pre-mv state to path. Production: always set.
-lock-timeout=0Override the lock wait timeout.
-state=pathOperate on a local state file instead of the backend.

-dry-run is the first command to run, every time. It resolves the source address against the current state and prints what it matched, without writing anything:

# READ-ONLY: shows what the mv would match; changes nothing.
terraform state mv -dry-run 'aws_instance.web' 'aws_instance.app'

The dry run tells you what the address matched, not what the state will look like afterwards, so it is a check on the address, not a substitute for a backup. Where the mv is large or unfamiliar, run it against a copy of the state as well and diff the result:

terraform state pull > /tmp/state-copy.json
terraform state mv -state=/tmp/state-copy.json 'aws_instance.web' 'aws_instance.app'
# Inspect /tmp/state-copy.json for the result
diff <(jq -S . /tmp/state-before-mv.json) <(jq -S . /tmp/state-copy.json)
# Then run the mv against the live state

Common mv operations

Rename in place. Change the local name from web to app:

terraform state mv 'aws_instance.web' 'aws_instance.app'

Re-key a for_each. Migrate from count-based instances to for_each-based instances with stable keys. The instance keys change from indices to strings:

terraform state mv 'aws_instance.web[0]' 'aws_instance.web["api"]'
terraform state mv 'aws_instance.web[1]' 'aws_instance.web["worker"]'

Move into a module. With the destination block pre-existing in module.network:

terraform state mv 'aws_vpc.main' 'module.network.aws_vpc.main'

The destination address must be valid in the current configuration (the destination resource block must exist).

Worked example: rename across the team

A team is refactoring the production configuration. They rename aws_db_instance.primary to aws_db_instance.main in the configuration, in the same pull request as a new read-replica.

# In the same change window, before applying:
terraform state mv 'aws_db_instance.primary' 'aws_db_instance.main'

# Verify
terraform plan
# Expected: 0 to add, 0 to change, 0 to destroy.

# Apply the configuration change
terraform apply
# Expected: only the new read-replica is created.

The alternative — using a moved block — would be:

moved {
  from = aws_db_instance.primary
  to   = aws_db_instance.main
}

Followed by:

terraform plan
# Expected: "Move aws_db_instance.primary to aws_db_instance.main"
# Then "No changes. Your infrastructure matches the configuration."

Either works. The moved block is reviewable in the pull request; the state mv is logged in the CLI history.

Validation

READ-ONLY

# Confirm the source address no longer exists
terraform state list | grep 'aws_instance.web'
# (empty)

# Confirm the destination address now exists
terraform state list | grep 'aws_instance.app'
# aws_instance.app

# Confirm the plan is empty
terraform plan
# No changes. Your infrastructure matches the configuration.

# Confirm the serial advanced (the mv was a state write)
terraform state pull | jq '.serial'
# 28  (was 27)

The serial advance is the audit trail. Every state write advances it; a missing advance means the mv did not run.

Production failure modes

Symptom: “Error: Cannot move from X to Y: Y is already in state”. Cause: the destination address is already used. Either the configuration is duplicating an address, or a previous mv already moved the resource. Investigate with state list; do not retry.

Symptom: plan after mv proposes to destroy the new address and create the old. Cause: the destination configuration does not match what the mv put in state, or the mv was wrong. Restore from the backup file with the backend’s restore procedure.

Symptom: plan after mv errors with “Resource not found”. Cause: the destination address is misspelled, or the module path is wrong. Confirm with state list and the configuration.

Symptom: mv succeeded but the real-world resource has a different attribute than expected. Cause: the resource was already drifted from configuration. mv does not refresh; it renames the cached attributes. Run a refresh-only plan to update the cache.

Symptom: lock acquire failed during mv. Cause: another apply is in progress. Wait for it to complete, or break the lock with team agreement.

Recovery

  1. Stop. Do not apply anything else.
  2. Pull the pre-mv state from the backup file: terraform state push /tmp/mv-backup.tfstate (only valid if the backend supports push; S3 and Terraform Cloud do not — use the backend’s restore procedure instead).
  3. For S3: copy the backup file over the live state object. Use versioning: restore the previous version of the state object.
  4. Verify with terraform plan — expect the original addresses.
  5. Investigate what went wrong with the mv before retrying.

What comes next

The next lesson covers terraform state rm — removing a resource from state without destroying it. The companion operation: when you want to stop managing something without deleting it.

Verification

  • You can describe the production procedure for state mv: backup, confirm addresses, run, verify plan is empty.
  • You can choose between state mv and a moved block for each refactor scenario.
  • You can recognise the error messages for missing or duplicated destination addresses.
  • You can recover from a failed state mv using the backup file.

Knowledge check · 7 questions

  1. Q1. What does `terraform state mv` do?

  2. Q2. Which flag writes a backup of the state before a state mv?

  3. Q3. terraform state mv is the right tool for every configuration refactor that changes an address.

  4. Q4. After running `terraform state mv aws_instance.web aws_instance.app`, the next plan should show:

  5. Q5. Which of these are valid uses for `terraform state mv`? (Select all that apply.)

  6. Q6. Before running a state mv, what is the minimum pre-flight?

  7. Q7. A team renames aws_db_instance.primary to aws_db_instance.main in a configuration pull request. They want the rename to be reviewable in the same PR. Which approach is preferred?

Passing score: 75%. Answers are checked in this browser.