Moving & Removing State
Terraform tracks every resource by its address in state. When you rename a resource, move it into a module, or split a configuration apart, Terraform sees the old address disappear and the new one arrive — and its default reaction is to destroy the old object and create a new one. For stateful infrastructure like databases, load balancers, or DNS records, that is catastrophic. This page covers the tools that let you refactor freely while keeping real resources untouched: moved blocks, removed blocks, and the imperative terraform state mv / terraform state rm commands.
Why renaming triggers a destroy
A resource address such as aws_instance.web is the key Terraform uses to map configuration to a real-world object recorded in state. Change that key and Terraform has no idea the old and new are the same thing.
# Before
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
Rename the label to app and a plan reveals the danger:
Output:
Plan: 1 to add, 0 to change, 1 to destroy.
# aws_instance.web will be destroyed
# (because aws_instance.web is not in configuration)
- resource "aws_instance" "web" { ... }
# aws_instance.app will be created
+ resource "aws_instance" "app" { ... }
Moved blocks
Since Terraform 1.1 (and OpenTofu from its first release), the moved block declares that one address is the successor of another. Terraform updates state to follow the move instead of replacing the resource. Because it lives in configuration, the refactor is versioned, reviewable, and reproducible across every workspace and teammate — unlike a one-off CLI command.
resource "aws_instance" "app" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
moved {
from = aws_instance.web
to = aws_instance.app
}
Now the plan is a no-op for the underlying object:
Output:
Terraform will perform the following actions:
# aws_instance.web has moved to aws_instance.app
resource "aws_instance" "app" {
id = "i-0abc123def4567890"
# (no changes)
}
Plan: 0 to add, 0 to change, 0 to destroy.
moved blocks handle every common refactor — renaming, adding a count or for_each index, and moving resources into or out of modules:
# Resource pulled into a module
moved {
from = aws_instance.app
to = module.compute.aws_instance.app
}
# Single resource converted to for_each
moved {
from = aws_subnet.private
to = aws_subnet.private["us-east-1a"]
}
Keep
movedblocks in the codebase for at least one release cycle so every workspace and collaborator processes them. Once you are confident all state files have caught up, you can safely delete the blocks.
Removed blocks
To stop managing a resource without destroying it, use a removed block (Terraform 1.7+, OpenTofu 1.7+). Deleting the resource block alone would schedule a destroy; the removed block instead drops the object from state and leaves it running in the cloud.
removed {
from = aws_instance.legacy
lifecycle {
destroy = false
}
}
Output:
# aws_instance.legacy will no longer be managed by Terraform, but will not be destroyed
# (destroy = false is set in the removed block)
Plan: 0 to add, 0 to change, 0 to destroy.
Set destroy = true if you actually do want Terraform to delete it as part of removing it from configuration.
Imperative state commands
The CLI equivalents mutate state directly without a configuration change. They are useful for ad-hoc surgery, but they are not recorded anywhere, so a teammate replaying history will not reproduce them — prefer moved/removed blocks for anything that lands in version control.
# Rename or relocate an address in state
terraform state mv aws_instance.web aws_instance.app
# Move a resource into a child module
terraform state mv aws_instance.app module.compute.aws_instance.app
# Forget a resource (leaves the real object alone)
terraform state rm aws_instance.legacy
Output:
Move "aws_instance.web" to "aws_instance.app"
Successfully moved 1 object(s).
Always run
terraform planafter astate mvorstate rmto confirm the result is a clean no-op (formv) or that nothing unexpected is now flagged for creation (forrm). Take a backup first —state rmdoes not touch real infrastructure, but a mistake forces you to re-import.
Choosing the right tool
| Goal | Use | Destroys real resource? | Versioned in code |
|---|---|---|---|
| Rename / restructure address | moved block | No | Yes |
| Stop managing, keep resource | removed block (destroy = false) | No | Yes |
| Stop managing and delete | removed block (destroy = true) | Yes | Yes |
| Quick local address change | terraform state mv | No | No |
| Forget a resource imperatively | terraform state rm | No | No |
Best Practices
- Reach for declarative
movedandremovedblocks first; they are reviewed, repeatable, and self-documenting in pull requests. - Always inspect the plan after any refactor and confirm it reports
0 to destroybefore applying. - Keep
movedblocks around for at least one full release cycle so every workspace and teammate processes them before cleanup. - Back up your state (or rely on remote backend versioning) before running
terraform state mvorstate rm. - Use
removedwithdestroy = falseto hand ownership of a resource to another configuration or to retire Terraform management gracefully. - Refactor in small, isolated commits so a single
movedorremovedchange is easy to review and roll back.