TerraformX · State Operations: Read, Move, Remove, ImportProduction Terraform
state mv: Renaming Without Recreation
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
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
movedblock). - Re-key a
for_eachresource to a stable identifier. - Move a resource between modules (with the destination block pre-existing in configuration).
- Recover from a
state rmperformed 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
movedblock 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:
| Flag | Purpose |
|---|---|
-dry-run | Report the instances the source address matches, without moving any of them. |
-backup-file=path | Write the pre-mv state to path. Production: always set. |
-lock-timeout=0 | Override the lock wait timeout. |
-state=path | Operate 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
- Stop. Do not apply anything else.
- 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). - For S3: copy the backup file over the live state object. Use versioning: restore the previous version of the state object.
- Verify with
terraform plan— expect the original addresses. - 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 mvand amovedblock for each refactor scenario. - You can recognise the error messages for missing or duplicated destination addresses.
- You can recover from a failed
state mvusing the backup file.
Knowledge check · 7 questions
Q1. What does `terraform state mv` do?
Q2. Which flag writes a backup of the state before a state mv?
Q3. terraform state mv is the right tool for every configuration refactor that changes an address.
Q4. After running `terraform state mv aws_instance.web aws_instance.app`, the next plan should show:
Q5. Which of these are valid uses for `terraform state mv`? (Select all that apply.)
Q6. Before running a state mv, what is the minimum pre-flight?
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.