Terraform identifies every resource by its address, such as aws_s3_bucket.logs or module.network.aws_vpc.main. When you rename a resource, move it into a module, or switch from count to for_each, the address changes. Terraform sees an old address with no code and a new address with no state, and plans to destroy the real object and create a new one.
Three configuration blocks solve this without touching state from the command line. A moved block (Terraform 1.1 and later) tells Terraform an object has a new address. A removed block (Terraform 1.7 and later) stops managing an object, with or without destroying it. An import block (Terraform 1.5 and later) adopts an object that already exists. All three live in code, show up in the plan, and go through the same review as any other change.
The moved Block
Add a moved block with the old and new address, and Terraform updates state instead of replacing the object. The most common case is moving a resource into a module:
moved {
from = aws_s3_bucket.logs
to = module.logging.aws_s3_bucket.this
}
The plan reports that the resource has moved and shows no create or destroy. The same pattern covers renaming a resource, renaming a module call, and moving instances when you change how they are counted:
moved {
from = aws_instance.web[0]
to = aws_instance.web["primary"]
}
Since Terraform 1.8, a moved block can also move an object to a different resource type when the provider supports that move, which helps when a provider splits one resource type into several.
When to Delete a moved Block
A moved block only matters until every state that referenced the old address has been applied with it. In a root module you control, delete it once every workspace and environment has applied the change.
In a shared module, keep it. Anyone who upgrades from an older version of your module still has the old address in their state, and removing the moved block turns their upgrade into a destroy and create. HashiCorp's guidance treats removing a moved block from a published module as a breaking change.
moved vs terraform state mv
Before moved existed, teams ran terraform state mv to rewrite state directly. That still works, but it changes state with no plan and no review, and it has to be repeated in every workspace and environment, by whoever has access to state. A moved block is reviewed in a pull request, appears in the plan, and applies itself everywhere the configuration runs. Use the CLI command for one-off repairs, and moved for refactoring.
The removed Block
A removed block tells Terraform to stop managing a resource. By default it destroys the object. With destroy = false, Terraform only forgets it:
removed {
from = aws_instance.legacy
lifecycle {
destroy = false
}
}
Use this when another configuration, another team, or another tool takes ownership of the resource, or when you split one large configuration into several. It replaces terraform state rm the same way moved replaces terraform state mv: the change is visible in a plan before it happens.
The import Block
An import block brings an existing object under Terraform without recreating it. Pair it with terraform plan -generate-config-out=generated.tf to draft the matching configuration:
import {
to = aws_s3_bucket.logs
id = "acme-prod-logs"
}
Review the generated code, run the plan until it shows the import with no other changes, then apply. The guide to generating Terraform from existing AWS resources covers this in detail, including the edge cases.
When ignore_changes Is the Right Tool
Sometimes the problem is not a new address but an attribute that keeps changing outside Terraform. The lifecycle meta-argument's ignore_changes tells Terraform not to correct specific attributes:
resource "aws_ecs_service" "api" {
# ...
lifecycle {
ignore_changes = [desired_count]
}
}
That is correct when another system legitimately owns the value, such as an autoscaler setting desired_count or a scanner writing a tag. It is the wrong fix for unexplained drift, because it hides the difference instead of explaining it. Keep the list narrow, avoid ignore_changes = all, and find out who is changing the value before you silence it.
A Safe Refactoring Checklist
- Put the refactor in its own pull request, with no functional changes mixed in.
- Add a
movedblock for every address that changes, and aremovedblock for anything leaving this configuration.
- Run a plan and expect zero creates and zero destroys. Any replacement means an address was missed.
- Apply in every workspace and environment that uses the configuration.
- Delete
movedblocks from root modules afterward; keep them in shared modules.
OpenTofu supports moved, removed, and import blocks too; check your version before relying on removed with lifecycle { destroy = false }.
Reviewing Refactors Before They Apply
The reason these blocks beat CLI state commands is review: the plan shows what will move, what will be forgotten, and what will be destroyed before anyone approves. ops0 runs Terraform and OpenTofu plans in review with policy checks and an approval step where the person who starts a deployment cannot approve it, so a refactor that would replace a database shows up in the plan before anyone approves it.
ops0 also runs drift detection against your clouds, which helps with the ignore_changes question: when an attribute keeps changing, you can see the change and decide whether code or the cloud is right, instead of hiding it. See drift prevention.
What does the moved block do in Terraform?
It tells Terraform that a resource or module now lives at a different address, so Terraform updates state instead of destroying and recreating the object. It requires Terraform 1.1 or later and shows up in the plan as a move with no create or destroy.
When can I remove a moved block?
In a root module you control, once every workspace and environment has applied it. In a shared or published module, keep it, because anyone upgrading from an older version still has the old address in state and would otherwise get a destroy and create.
What is the difference between the removed block and terraform state rm?
Both stop Terraform from managing a resource. The removed block does it in code, shows up in the plan, and can either destroy the object or, with destroy = false, leave it running. terraform state rm edits state directly with no plan or review.
Does the moved block work with modules and for_each?
Yes. You can move resources into or out of modules, rename module calls, and move instances between count and for_each keys, such as from aws_instance.web[0] to aws_instance.web["primary"].
Does ignore_changes stop drift detection?
For the listed attributes, yes: Terraform stops proposing changes to them, so drift in those attributes no longer appears in the plan. Use it only for attributes another system legitimately owns, and keep the list narrow.
- moved block reference: HashiCorp. Syntax and behavior of moved blocks.
- Refactor modules: HashiCorp. Moving resources and modules, and removing moved blocks.
- removed block reference: HashiCorp. Removing resources from state with or without destroying them.
- Import existing resources: HashiCorp. Import blocks and configuration generation.
- lifecycle meta-argument: HashiCorp. ignore_changes and other lifecycle rules.