Skip to content
intermediate Phase 6 · State Operations, Import, and Relocation

Moving and Forgetting State Entries

Relocating addresses after refactors and dropping objects Terraform should no longer track.

55m
0 problems
Topic Progress 0%

terraform state mv: Renames That Touch Nothing in the Cloud

You rename resource "aws_instance" "web" to resource "aws_instance" "app" and run plan. Terraform answers with the most alarming diff in its vocabulary: destroy aws_instance.web, create aws_instance.app. No API call has happened yet — Terraform simply sees one address it manages and one address it does not, and reconciles the gap the only way it knows. terraform state mv closes that gap on Terraform's side without touching the cloud:

terraform state mv 'aws_instance.web' 'aws_instance.app'
terraform plan   # must show: No changes

The command reassigns an address inside state — that is the entire operation. The instance keeps its ID, its public IP, its running processes, and its billing line; only Terraform's memory changes. Treat the empty plan afterward as part of the command, not an optional courtesy: if the follow-up plan proposes any change to the moved object, something else about the refactor also shifted (a changed argument, a different availability zone data source) and you want to see that before apply.

The address forms you inspected with state list are exactly what mv accepts, and quoting is load-bearing. Move a counted element to a for_each key during a count→for_each conversion:

terraform state mv 'aws_sns_topic.events[0]' 'aws_sns_topic.events["orders"]'
terraform state mv 'aws_sns_topic.events[1]' 'aws_sns_topic.events["payments"]'

Move a resource out of (or into) a module:

terraform state mv 'module.legacy.aws_s3_bucket.artifacts' 'aws_s3_bucket.artifacts'

Move an entire module and every address beneath it:

terraform state mv 'module.network' 'module.infra'

That last form is the one to remember for reorganizations — you do not move forty subnets one command at a time. Throughout, single-quote both addresses: unquoted brackets and asterisks invite shell expansion, and a mangled address in a mutation command is a category of mistake that state list would have prevented (Listing and Showing State Entries). One structural note on safety: with a remote backend the command takes the state lock for the duration of the write, so a teammate's concurrent apply cannot interleave; with local state there is no such protection, which is one more entry on the list of reasons local state failed teams. And when the refactor lives in configuration that other people have already cloned, a one-off state mv only fixes your copy of reality — their next run recreates the destroy/create diff. Recording the move in code with a moved block is the team-safe form of the same operation, and it is the subject of moved Block Declarations.

mv has one more direction worth practicing: between types, converting a resource into an equivalent managed by a different provider resource when an API migration forces it (an aws_s3_bucket-plus-separate-acl pattern consolidating into aws_s3_bucket with inline configuration). The command happily rewrites the address prefix, but the attributes must still match — a moved entry whose recorded arguments no longer correspond to the new type's schema shows up as a garbage diff on the next plan, and the honest response is to re-import under the new address instead of forcing a move that lies about the object's identity. Run that follow-up plan every single time; it is the command's receipt.

terraform state rm: Forgetting Without Destroying

terraform state rm <address> removes an entry from state and leaves the real object untouched in the cloud. Terraform forgets; nothing is deleted. That single sentence contains both the command's usefulness and its danger, because the next plan now sees an unmanaged object where configuration expects one — or no object at all.

The safe, common use is retiring a resource from a configuration without destroying it. Compare the two paths for removing aws_s3_bucket.logs from your config:

Action What happens to the bucket
Delete the resource block, run apply Terraform destroys the bucket (and its objects)
Run terraform state rm 'aws_s3_bucket.logs', delete the block, apply Bucket survives; Terraform no longer knows it exists

If the bucket must outlive the configuration — a data bucket being handed to another team, an RDS instance mid-migration — state rm first, then remove the block, and the apply diff drops to nothing for that object. Skip the state rm and the destroy is in the plan, which is exactly where you want to catch it rather than in the incident channel afterward.

The second use is adoption hand-off: moving a shared object from one state file to another. Two stacks, one object:

# in the stack that currently manages it
terraform state rm 'aws_s3_bucket.shared-artifacts'
# in the adopting stack, after writing the matching resource block
terraform import 'aws_s3_bucket.shared-artifacts' shared-artifacts

The object is never destroyed and never recreated; ownership of its state entry transfers. Two disciplines keep this honest. First, write the resource block in the adopting configuration before the rm — between the two commands the object belongs to nobody, and any plan run in that window against the first stack without the block removed will propose recreating it. Second, remember that an orphan is invisible insurance-wise: cloud cost continues, drift detection stops, and only the adopting stack's plan will ever mention the object again. Announce the hand-off in the same change window on both repositories; the technical transfer takes seconds, and the organizational one is what actually fails in practice.

The mirror-image trap deserves its own warning. state rm on an address that is still in configuration does not remove the object — it removes Terraform's memory of it. The very next plan proposes creating the resource afresh, and if an object with the same name already exists, the apply fails on a naming conflict or, with namespaced resources, quietly makes a second one. state rm is a scalpel for objects leaving a configuration, never a cleanup tool for objects still declared in it. One operational habit rounds out the command: when you rm in bulk — an entire count-based set, everything under a retiring module — pipe the address list from state list into a script rather than retyping addresses, and state pull a backup first. Bulk mutation is where single-command discipline meets the arithmetic of mistakes: one wrong address in a loop of fifty is fifty wrong addresses when the loop runs twice.

state pull, Backups, and the Surgery Discipline

Every command in this topic edits the single file that knows how your infrastructure maps to reality. Treat state mutations the way a database administrator treats a manual UPDATE on a production table: with a backup, a reason, and a verification step.

The backup command is terraform state pull — it downloads the current state exactly as JSON to stdout, straight from whatever backend init configured:

terraform state pull > backups/terraform.tfstate.$(date +%Y%m%d-%H%M%S).json

Run it before any mv, rm, or import, against the very same backend the mutation will touch. The file it produces is plain JSON, so it doubles as an audit artifact (what did state look like before the surgery?) and as the raw material for scripted repairs. Terraform also writes its own safety net — every state write leaves a terraform.tfstate.backup beside the local state file — but that net covers only local-state workflows; a botched remote mutation needs your pull-backup.

Restoring is terraform state push, and its quirks are worth knowing before you need it in anger:

terraform state push backups/terraform.tfstate.20260110-091500.json

Push validates the backup's serial and lineage against the current state and refuses to overwrite a newer state (a higher serial) without -force; -force is how you accept "yes, I intend to roll back state to an earlier write." Because the check exists, a backup taken five minutes ago pushes cleanly, while one from last month tells you plainly that state has moved on — the guardrail doing its job.

Two more mechanics round out the discipline. Each mutation writes a fresh backup of the pre-mutation state (terraform.tfstate.backup, or the .backup sibling a remote backend leaves next to its cached copy), so even a no-backup run is rarely unrecoverable — but relying on that is gambling on local disk you do not control in CI. And -backup=false exists on mv, rm, and import for bulk operations where hundreds of backup files would be noise; disable it only after state pull has given you an artifact you trust. One last habit separates operators from tourists: name the backup files by intent, not just by timestamp — pre-mv-module-rename.json tells your future self why the snapshot exists when a timestamp only tells you when.

The full ritual, then, for any state mutation on anything you care about:

terraform state list 'aws_s3_bucket.*'      # 1. confirm addresses exist
terraform state pull > pre-mv.json           # 2. snapshot
terraform state rm 'aws_s3_bucket.legacy'    # 3. mutate, one object per command
terraform state list 'aws_s3_bucket.*'      # 4. confirm the new inventory
terraform plan                               # 5. confirm the diff you expect — and nothing more

Five steps, every time, on anything you would be unhappy to explain losing. The commands are individually simple; the ritual is what makes them safe to run against production infrastructure on a Friday afternoon. When the object you need is not in state at all but does exist in the cloud, moving and forgetting are the wrong tools entirely — that is adoption, and it has its own command: Importing Existing Resources.

Quiz

1. You rename a resource block's local name and apply without doing anything else. What happens?

Question 1 options

2. What does `terraform state rm 'aws_s3_bucket.logs'` do to the bucket itself?

Question 2 options

3. A shared S3 bucket must move from stack A's state to stack B's state with zero interruption. What is the correct sequence?

Question 3 options

4. Why run `terraform state pull > backup.json` before a state mv, and what guards an accidental bad push afterward?

Question 4 options

Flashcards

Question

What does `terraform state mv` change, and what does it never change?

Answer

It reassigns an address inside state (old → new), including across modules and between count indexes and for_each keys, and for whole modules (`module.a` → `module.b`). It never touches the cloud object — same ID, same attributes, zero downtime — so the verifying plan must show No changes.

Question

state rm versus deleting a resource block: what is the difference in outcome?

Answer

Deleting the block and applying DESTROYS the object via the API. `state rm` removes only the state entry — the object survives, Terraform forgets it — which is how you retire or hand off an object without destruction. rm first, then delete the block, and the apply diff for that object disappears.

Question

How do you transfer a live shared object from one state file to another?

Answer

In the losing stack: `terraform state rm <address>`. In the adopting stack: author the matching resource block first, then `terraform import <address> <id>`. The object is never destroyed; only state ownership moves. Write the new block before the rm so the object is never unmanaged mid-handoff.

Question

What is the state-mutation ritual, and why does each step exist?

Answer

1) state list to confirm exact addresses; 2) `terraform state pull > backup.json` to snapshot; 3) mutate — one object per command, addresses single-quoted; 4) state list again to confirm the new inventory; 5) plan to verify the diff is exactly what you intended and nothing more.

Question

How does terraform state push protect against a bad restore, and when is -force required?

Answer

Push compares the backup's serial/lineage with the current state and refuses to overwrite a newer state (higher serial). -force overrides that guard — it is your explicit statement of intent to roll state back to an earlier write, not a routine flag.

Revision Notes

Key Takeaways

  • 1. state mv reassigns addresses in state only — the cloud object is never destroyed or recreated, and the empty follow-up plan is part of the operation
  • 2. Address forms carry over from state list: count indexes, for_each keys, module prefixes, and whole-module moves all work, with single-quoting as a hard habit
  • 3. state rm orphans rather than destroys — correct for retire-and-handoff, catastrophic when the object is still declared in configuration (next plan recreates it)
  • 4. Cross-state adoption is rm-then-import, with the adopting block written first so the object is never unmanaged mid-transfer
  • 5. Pull a JSON backup before every mutation and let push's serial guard govern restores; the five-step ritual is what makes state surgery repeatable instead of brave

Interview Tips

  • • "You renamed a resource and the plan shows destroy/create. What do you do?" — do not apply; run state mv with single-quoted addresses, then plan to confirm No changes; explain that the local name is the state identity
  • • "state rm vs deleting the block — when is each right?" — deleting the block destroys on apply; state rm preserves the object and forgets it — right for retirement and handoff, wrong for anything still declared
  • • "How would you move a resource between two Terraform states with no downtime?" — state rm in the owner, import in the adopter, block written before the rm; mention verifying with plan on both sides
  • • "What is your safety procedure for manual state edits?" — state list to confirm addresses, state pull to snapshot, one mutation per command, plan to verify; note push's serial/lineage guard and when -force is legitimate

Cheat Sheet

State mv and rm

Rename safely:

terraform state mv 'aws_instance.web' 'aws_instance.app'
terraform state mv 'aws_sns_topic.events[0]' 'aws_sns_topic.events["orders"]'
terraform state mv 'module.legacy.aws_s3_bucket.a' 'aws_s3_bucket.a'
terraform state mv 'module.network' 'module.infra'   # whole module
terraform plan                                       # must show: No changes

Forget safely (object survives!):

terraform state rm 'aws_s3_bucket.logs'   # orphan entry; object untouched in cloud
Goal Action
keep object, stop managing it state rm, then delete the block
destroy the object delete the block and apply (no rm!)
move object to another state rm here → write block there → import there

Before/after every mutation:

terraform state pull > pre-mv.json          # snapshot (JSON, auditable)
terraform state push pre-mv.json            # restore; refuses newer serial without -force

Rules: single-quote addresses; one object per command; team refactors use moved blocks (next topic) so everyone's state migrates; the next plan is the verification step, not optional.

Continue Learning