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

moved Block Declarations

Recording refactors in code so state moves automatically for every teammate.

55m
0 problems
Topic Progress 0%

moved Blocks: The Refactor Recorded in Code

You rename a resource, and Moving and Forgetting State Entries taught you the fix: terraform state mv, one command, on your machine. Then a teammate pulls your branch and runs plan — and sees the destroy/create diff you thought you had solved. The command fixed your state; their state, CI's state, and the staging pipeline's state never heard about it. moved blocks solve exactly that problem by putting the relocation in version control:

moved {
  from = aws_instance.web
  to   = aws_instance.app
}

The block declares a source address and a destination address. Terraform applies it during planning: the next terraform plan by anyone holding this configuration migrates their state automatically, and the plan output says so explicitly:

Note: Objects have moved

aws_instance.web has moved to aws_instance.app

No changes. Your infrastructure matches the configuration.

Compare that with the previous diff — destroy one instance, create another — and the value is obvious. The cloud object is never touched; only the mapping from address to object changes, identical in effect to state mv but executed by every plan-runner instead of by whoever remembered to run the command. The move becomes part of the pull request: reviewers see the refactor and its state migration in one diff, and git log answers "when did this resource get renamed, and who migrated state" months later.

The semantics have precise edges worth knowing. The source must exist in state and the destination must be declared in configuration — terraform validate catches a to address with no matching resource block, so a typo'd destination fails before any state is involved. A destination address that is already occupied in state is an error too: you cannot move an object on top of another object. And when the plan runs, the move happens before Terraform compares configuration to state, which is why the very same plan shows the relocation note and then reports No changes — the refactor is invisible to the diff once the move is applied. That ordering also means a single plan can carry the move and the next real change together; the move never masquerades as a resource action. The block itself carries no executable logic and never reaches a provider API — it is a state directive that Terraform consumes and discards, which is why it can live anywhere in your .tf files and why adding one cannot change what your infrastructure does, only what Terraform remembers.

One honest limitation completes the picture: a moved block records a move, not a history. Once everyone's state has migrated, the block has done its job — but until you remove it, it stays in every plan. Cleanup, and the warning Terraform gives when a block's source no longer exists, are the next chapter's first subject; module-scale moves come before that. One practical tip for writing these blocks at review time: keep the from and to on adjacent lines and the resources grouped by refactor, so a reviewer reading the diff sees the mapping instantly — a moved block is documentation of intent as much as it is an instruction to Terraform, and a diff full of scattered, uncommented moves is much harder to trust than one that reads like a table.

Moving Whole Modules and Crossing Module Boundaries

A rename at the resource level is the tutorial case; real refactorings happen at module scale. You reorganize network into infra, promote modules/app_v1 to modules/app, or merge two layers — and each reorganization renames a module. prefix, which would otherwise orphan every address beneath it:

moved {
  from = module.network
  to   = module.infra
}

One block moves everything. Every resource, data source, and output beneath module.network — module.network.aws_vpc.main, module.network.aws_subnet.private["eu-west-1a"], all of it — relocates to the matching address under module.infra in a single state transformation. You do not enumerate forty addresses; you declare the prefix change and Terraform rewrites the tree. This is the single strongest argument for moved over state mv at scale: the module-level mv command exists (terraform state mv 'module.network' 'module.infra'), but it is still one imperative act per machine, while the block migrates every checkout on its next plan.

Crossing module boundaries works with the same syntax when an object changes owner rather than prefix:

moved {
  from = module.legacy.aws_s3_bucket.artifacts
  to   = aws_s3_bucket.artifacts
}

Here the object is promoted from a child module into the root (or the reverse, or between two siblings). Terraform matches the object under the new address to the same cloud instance, and the plan reports the move plus — this is the part to check — no resource actions for it. If the promoted object's plan shows an in-place update, something besides the address changed in transit (different provider configuration, different argument defaults between the two module versions), which is precisely the moment to stop and reconcile rather than apply over it.

Module moves pair naturally with changing a module's source (a version bump that renames internals) and with the module layering refactor topics ahead: the pattern is always the same — declare the from/to prefix change in the same pull request as the reorganization, so the migration travels with the code. Two edge cases keep this honest. First, moved blocks chain: a → b in one commit and b → c in the next resolve correctly when Terraform processes both, which is how an in-flight refactor survives being re-rebased. Second, moving into a module requires that module's arguments to produce the same effective configuration — a moved block relocates identity, not settings; changed inputs still plan their own diffs, and that separation is a feature: you see the reorganization as a no-op and every behavioral change as its own line in review. The module-level move is also the recovery tool of choice when a child module's source URL changes but the underlying objects must not: point the call at the new source, declare the prefix move, and the plan confirms the objects followed the address rather than being recreated at the new location. Verify with terraform state list module.infra after the plan — every former address should now appear under the new prefix, which turns a scary reorganization into a checkable assertion. If any old address is still listed, the move did not cover it, and you have caught that in review rather than in an outage.

Stale Moves, Warnings, and the Cleanup PR

Six months after the refactor, every teammate's state long since migrated, and the moved block is still sitting in main.tf. Its source address no longer exists in anyone's state — so Terraform stops treating it as a migration and starts treating it as a question, warning on every plan:

Warning: Moved block source no longer exists

The 'moved' block for aws_instance.web refers to a source address that
no longer exists in state. Remove the block if the migration is complete.

The warning is harmless and correct: the block's job finished when the last engineer's and pipeline's states migrated, and Terraform has no way to know your team is done — so it keeps asking. The fix is a small cleanup PR deleting the block, and the discipline is scheduling that PR deliberately: leave the block in place for one or two release cycles (long enough for every long-lived branch and the slowest pipeline to plan at least once), then remove it. A team that deletes the block in the same PR as the rename wins nothing: anyone who pulls only the rename commit — cherry-picks, partial rebases, a stale CI cache — gets the destroy/create diff with no migration in sight. A team that never deletes it accumulates one warning per refactor until plan output is unreadable, and unreadable plan output is how real changes get rubber-stamped.

Three failure modes complete the topic, and each has a distinct signature. A moved block whose destination is not in configuration fails at validate — the typo case, caught early. A moved block into an occupied destination fails at plan with a conflict — two objects cannot share one address, and the message names both. And a moved block shipped while the source still exists in some states but not others (half the team migrated via an earlier state mv, half via the block) resolves cleanly for everyone, because the operation is idempotent per state: it moves the object if the source is present, and warns-if-stale otherwise. That idempotency is why mixed tooling during a transition window is safe. One more scenario deserves a line because it produces a silent surprise rather than an error: a moved block whose source address is never going to appear in a given state, because that environment never created the object in the first place (a workspace where count evaluated to zero). That state gets the stale warning and nothing else — harmless, but worth recognizing so you do not read it as a failed migration.

The full comparison, since interviews ask for exactly this discrimination:

Tool Lives in Migrates whose state Best for
terraform state mv one shell only the operator's emergency surgery, uncommitted experiments
moved block configuration (git) everyone, on next plan team refactors, module reorganizations
terraform import / import block shell / git the operator / everyone objects not yet in state (adoption)

With refactors now declarative, the phase's last subject is the failure that stops all operations dead: a state lock that outlived the process that took it — State Lock Breakage and Recovery.

Quiz

1. What does a `moved` block do when the next terraform plan runs?

Question 1 options

2. Why does a moved block beat `terraform state mv` for a team refactor?

Question 2 options

3. `moved { from = module.network, to = module.infra }` is declared and the module was renamed in the same PR. What does the next plan show?

Question 3 options

4. Plans now warn that a moved block's source address no longer exists. What does it mean and what is the fix?

Question 4 options

Flashcards

Question

What does a moved block declare, and when is it applied?

Answer

A `from` (source, must exist in state) and `to` (destination, must be declared in configuration) address. Terraform applies it during planning: the plan shows 'Objects have moved' and No changes for that object — no cloud API calls, and the migration happens for whoever runs plan, not once per operator.

Question

moved block vs terraform state mv vs terraform import — one line each.

Answer

state mv: imperative, migrates only the operator's state — emergency surgery. moved block: declarative in git, migrates everyone on their next plan — team refactors and module renames. import (command or import block): attaches objects NOT in state — adoption of pre-existing infrastructure.

Question

How does a module-level moved block behave?

Answer

`moved { from = module.network, to = module.infra }` relocates the entire subtree — every resource, data source, and output address beneath the prefix — in one state transformation. Also valid for single objects crossing module boundaries (module.a.aws_x.y → module.b.aws_x.y or into the root).

Question

What validations guard moved blocks, and what do the failures look like?

Answer

Destination not in configuration → error at validate (typo caught early). Destination already occupied in state → conflict error at plan (two objects cannot share one address). Source missing → warning, not error: benign staleness indicating the migration already completed.

Question

What is the cleanup discipline for moved blocks?

Answer

Leave the block for one to two release cycles so every long-lived branch and CI pipeline plans at least once, then delete it in a dedicated cleanup PR. Deleting it in the rename PR strands stragglers (cherry-picks, stale checkouts) with the destructive diff and no migration; never deleting it drowns plans in stale warnings.

Revision Notes

Key Takeaways

  • 1. A moved block relocates a state address during plan — from must exist in state, to must exist in configuration, and the cloud object is never touched
  • 2. Configuration-resident moves migrate every teammate's and CI's state on their next plan; the one-off state mv fixes only the operator's copy
  • 3. Module-level moves cascade: one block rewrites the entire address subtree, which makes large reorganizations a single reviewed declaration
  • 4. Validation catches destination typos at validate time and occupied destinations at plan time; a missing source degrades to a harmless stale warning
  • 5. Schedule the cleanup PR a release or two after the refactor — early deletion strands stragglers, eternal blocks drown plan output in warnings

Interview Tips

  • • "How would you safely rename a resource for a team?" — moved block in the same PR as the rename; mention the state mv fallback for one-off emergencies and why it does not scale to a team
  • • "moved block vs terraform import" — moved relocates addresses already in state (refactor); import attaches objects not in state (adoption); conflate them and you plan destruction
  • • "What does 'Note: Objects have moved' in a plan mean?" — Terraform executed declared relocations during planning; the follow-up is No changes, not an action — the move precedes diffing
  • • "A moved block warns its source does not exist — is that bad?" — no: migration is complete; Terraform cannot know your team is done, so it asks you to remove the block; the cleanup timing (not instant deletion) is the senior answer

Cheat Sheet

moved Blocks

Declare the refactor in config — everyone migrates on plan:

moved {
  from = aws_instance.web    # must exist in state
  to   = aws_instance.app    # must exist in configuration
}

moved {                       # one block moves the whole subtree
  from = module.network
  to   = module.infra
}
Note: Objects have moved
aws_instance.web has moved to aws_instance.app
No changes.

Rules & failure modes:

Situation Result
to not in config validate error (typo)
to already occupied in state plan conflict — two objects, one address
from missing from state warning: migration already complete — remove block
half the team used state mv fine: the block is idempotent per state

Choose the tool: state mv = one machine, emergency · moved = team, in git · import = object not in state at all.

Cleanup: keep blocks 1–2 release cycles (let every pipeline plan once), then delete in a dedicated PR.

Continue Learning