Refactoring with moved Blocks
When you rename a resource, move it into a module, or restructure your code, Terraform sees the old address vanish and a new one appear — and by default it plans to destroy the old object and create a new one. For a stateless resource that is wasteful; for a database or a volume it is an outage or data loss from what was meant to be a pure code cleanup.
The moved block (1.1+) tells Terraform "this is the same object under a new address." With it, a refactor becomes a no-op apply that just updates state's bookkeeping — the resource is never touched in AWS.
The Refactor Problem
Suppose you rename aws_instance.web to aws_instance.api to better reflect its role. Terraform compares state (which still has web) against config (which now has api) and reads it as two unrelated facts: web is gone, so destroy it; api is new, so create it. The plan shows one to destroy and one to add. Apply that and you have torn down a running instance and replaced it with a fresh one, for a change that was only ever a name.
The moved Block
A moved block records the rename so state follows the new address. You add it alongside the renamed resource, and the next plan reads zero changes instead of a destroy and a create — Terraform recognizes that web and api are the same object and simply re-keys it in state.
moved { from = aws_instance.web to = aws_instance.api } resource "aws_instance" "api" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t3.micro" }
Moving Into and Between Modules
The same block handles the bigger refactor of extracting resources into a module. When you move aws_instance.api inside a new web module, its address becomes module.web.aws_instance.this, and a moved block from the old root address to the new module address keeps state in step. This is what makes the common "we should pull this into a reusable module" cleanup safe instead of a destroy-and-recreate of everything you extract.
moved { from = aws_instance.api to = module.web.aws_instance.this }
moved vs State Surgery
Before moved blocks, the same re-keying was done with terraform state mv — an imperative local command. The block is the declarative replacement: it lives in code, shows up in the plan, and applies automatically for everyone who runs it, including CI. state mv bypasses all of that. It runs on one person's machine against the shared state and is invisible to review, which is why a shared refactor belongs in a moved block and surgery is reserved for genuine one-off fixes.
Lifecycle of a moved Block
A moved block is transitional. Keep it through the apply that performs the move and long enough for every teammate and every pipeline to have applied it — because anyone whose state still has the old address needs the block to avoid the destroy-and-recreate you dodged locally. Once you are confident all states have caught up, prune it; a stale moved block referencing addresses that no longer exist is harmless but is dead code cluttering the config.
moved block — lives in code, appears in the plan, and applies for everyone who runs it, making the refactor a reviewed change. Choose it for any rename or restructure in shared state.
terraform state mv — an imperative local command each person and pipeline must run separately, invisible to review. Reserve it for genuine one-off surgery on a single machine, not a refactor others must replay.
- Renaming a resource without a
movedblock and applying, destroying and recreating a production resource needlessly. - Removing the
movedblock too soon, before CI and teammates have applied it, so they get the destroy and recreate you avoided locally. - Using
state mvon shared state for a refactor everyone needs, so others' plans still diverge. - Getting the
fromortoaddress wrong — especially an index or module path — so the move does not match and Terraform recreates anyway. - Applying a module extraction without verifying a zero-change plan first, then discovering the addresses did not line up.
- Pair every resource rename or module extraction with a
movedblock so the refactor is a no-op apply. - Keep
movedblocks until you are confident all states — teammates and CI — have applied them, then prune. - Prefer
movedblocks overstate mvfor anything in shared state, reserving surgery for genuine one-off fixes. - Verify with a plan that the move produces zero changes before applying.
- Double-check index and module paths in the
fromandtoaddresses, since a mismatch silently falls back to destroy and recreate.
moved block is one of Terraform's stronger refactoring affordances
Knowledge Check
Why does renaming a resource trigger a destroy and recreate by default?
- Terraform sees the old address gone and a new one appear, and reads them as two unrelated resources
- Renaming changes an immutable attribute on the AWS object that always forces a full replacement
- State stores each resource by its line number in the file, so moving the block invalidates the entry
- The provider requires a new ID whenever the resource name changes
What does a moved block record?
- That the object at the old address and the new address are the same, so state re-keys instead of recreating
- A full backup of the resource's attributes taken just before the rename is applied
- An order constraint forcing the new resource to apply only after the old one has been fully destroyed first
- A request to import the resource fresh from AWS
Why prefer a moved block over terraform state mv for a shared refactor?
- It lives in code, shows in the plan, and applies for everyone including CI, rather than running once on one machine
- It is the only one of the two that can re-key resources living inside a child module that is shared across the whole team
- It runs faster than the command because it skips the refresh phase on every apply
- It encrypts the moved resource's state entry while the command leaves it in plaintext
When is it safe to remove a moved block?
- Once every teammate and pipeline has applied it, so no remaining state has the old address
- Immediately after you have applied it once on your own local machine
- Never — it must stay in the config permanently to keep the rename valid
- As soon as the plan first shows the destroy-and-recreate for the renamed resource on your own run
You got correct