You rename a Terraform resource and move it inside a module, and the plan now shows a destroy and a create. How do you make the refactor without touching the real infrastructure?
answer
- state is keyed by address
- from the old address, to the new one
- re-keys the entry during plan
- committed and reviewed, unlike state mv
- same resource type only
basics
~20 sAdd a moved block with the old address in from and the new address in to. Terraform then rewrites the state mapping during plan and apply instead of destroying and recreating the object. The alternative, terraform state mv, does the same thing imperatively and unreviewably.
solid answer
~60 sTerraform keys state by resource address, so renaming a resource or nesting it in a module looks to Terraform like the old address disappearing and a brand new one appearing — hence destroy plus create. A `moved` block tells it otherwise: `from` is the old address, `to` is the new one, and during plan Terraform re-keys the existing state entry rather than proposing any change to the object. The plan output says the resource has moved, and the rest of the diff should be empty. The imperative equivalent is `terraform state mv old new`, which does work, but it is a command someone runs once against the state — nothing in the repository records it, a colleague on an older checkout still sees the destroy, and it races with whoever else is applying. The `moved` block is committed with the refactor it belongs to, so review, CI and every collaborator converge on the same result, and it is idempotent, so running it again after the move is a no-op. `moved` blocks are supported from Terraform 1.1 onward.
code
hcl · 9 linesmoved {
from = aws_instance.web
to = module.app.aws_instance.web
}
moved {
from = aws_instance.web[0]
to = aws_instance.web["blue"]
}go deeper
Know that Terraform tracks resources by address, so a rename reads as delete plus create, and that a moved block with from and to prevents that.
Explain that the block re-keys the state entry during plan, that it handles renames, module moves and index-key changes, and that it is idempotent and limited to compatible endpoints.
Argue why the committed block beats terraform state mv on a shared estate: reviewability, CI convergence, and the race window between merging a rename and manually fixing state.
Set the team rule that state surgery arrives as code — moved, import and removed blocks — with CLI state subcommands reserved as an audited escape hatch, so no production change depends on someone remembering to run a command.
## Why a rename looks like a destroy State maps **addresses** to real objects. `aws_instance.web` is an address; so is `module.app.aws_instance.web`; so is `aws_instance.web["blue"]`. Terraform has no memory of what you *meant* — when it compares configuration to state and finds an address in state that is no longer in the configuration, the only conclusion available is that you want the object gone. Meanwhile the new address has no state entry, so Terraform proposes creating it. A purely cosmetic rename therefore renders as a destroy and a create, and on a database, a load balancer or anything with data or a fixed address, that is an outage. ## The moved block ```hcl moved { from = aws_instance.web to = module.app.aws_instance.web } ``` This is configuration, not a command. During plan, Terraform reads the `moved` blocks and re-keys matching state entries before it computes the diff, so the plan output reports that the resource has moved to the new address and — assuming nothing else changed — proposes no other action. The state rewrite is committed as part of apply. It works for the address shapes that refactors actually produce: - renaming a resource: `aws_instance.web` to `aws_instance.frontend` - moving a resource into or out of a module: `aws_instance.web` to `module.app.aws_instance.web` - moving an entire module call: `module.app` to `module.frontend` - changing instance keys: `aws_instance.web[0]` to `aws_instance.web["blue"]` Moves can be chained across releases — if a resource moved from A to B in one version and B to C in the next, keeping both blocks means a consumer jumping straight from A lands correctly on C. ## What it cannot do `moved` re-keys an existing entry; it does not convert one thing into another. The endpoints must be compatible — you cannot use it to change a resource's **type**, because a different type is a different provider schema and a genuinely different object. Migrating from one resource type to another is a real destroy-and-create, or an out-of-band creation plus an import. `moved` also lives inside a single configuration and its state: it cannot move a resource into a *different* state file, which is a separate operation entirely. And it is not a licence to ignore the plan. If you rename a resource *and* change an argument that forces replacement in the same commit, the moved block will re-key the entry and the plan will still show the replacement. Read what the plan actually says. ## moved versus terraform state mv ```bash terraform state mv aws_instance.web module.app.aws_instance.web ``` This achieves the same state rewrite immediately. It is the older way, and it still has uses — notably when no configuration exists to hang a block on. But as the default it is worse in every dimension that matters to a team: - **Not reviewable.** The refactor commit shows a rename; nothing in the repository says the state was fixed. A reviewer cannot tell a safe refactor from a destructive one. - **Not reproducible.** It affects whichever state the person's working directory pointed at. With a remote backend that is at least shared — but anyone on an older checkout, and any CI job that runs before the fix, plans a destroy. - **Order-sensitive and racy.** It mutates shared state outside the plan/apply cycle, taking the lock on its own. If a pipeline applies between the code merge and the manual `state mv`, it destroys the resource. - **Not idempotent in the same way.** Re-running it after the move errors, whereas a `moved` block that has already been applied is simply a no-op. The general principle in modern Terraform is that state surgery belongs in code: `moved` for refactors, `import` for adoption, `removed` for dropping management. The CLI subcommands are the escape hatch for cases the blocks do not cover. ## How long to keep the block A `moved` block is inert once every state that could contain the old address has been applied. In a single application repository with one state, that is the next apply, and you can delete it in a follow-up commit — though leaving it costs nothing but a few lines. In a shared module the calculus is different, because you do not control when consumers upgrade, and the block has to survive their deprecation window. ## Verify, do not trust The check is always the plan. `terraform plan` after adding the block should show the move and no destroy. If it still proposes destroying the old address, the `from` address is wrong — go back to `terraform state list` and copy the exact string, including index keys and module path.
- When would `terraform state mv` still be the right tool over a `moved` block?When there is no configuration to attach a block to — for example cleaning up a state entry whose configuration has already been deleted, or splitting resources out into a different state file, which `moved` cannot express because it operates within one state. Treat it as an escape hatch, run it deliberately, and record what you did.
- Can a `moved` block change a resource's type, say from one provider resource to another?No. The endpoints must be compatible; a different resource type has a different schema and is a genuinely different object. That migration is either a real replace, or create-the-new-thing plus `import` plus a `removed` block for the old one, depending on whether you can tolerate the object being recreated.
- You add a `moved` block and the plan still shows a destroy of the old address. What is wrong?Almost always the `from` address does not match what is in state — a missing module path, a wrong index key, or a typo. Run `terraform state list` and copy the address verbatim. The other possibility is that the block sits in a different module than the one whose state holds the entry.
- How does a `moved` block behave if it is applied twice?The second run is a no-op. Once the state entry lives at the `to` address there is nothing at `from` to move, and Terraform simply ignores the block. That idempotence is what makes it safe to leave in the repository and safe to run in CI, unlike `terraform state mv`, which errors when re-run.
saying these in an interview costs you the question
- Accepts the destroy-and-create because "Terraform knows best"
- Runs terraform state mv on a laptop and commits only the rename
- Thinks a moved block can change the resource type
- Believes moved blocks physically move anything in the cloud
- Deletes the moved block in the same commit that introduces the rename