Skip to content
Infrastructure as Code iac state 4 min read

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 moved blocks 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 plan after a state mv or state rm to confirm the result is a clean no-op (for mv) or that nothing unexpected is now flagged for creation (for rm). Take a backup first — state rm does not touch real infrastructure, but a mistake forces you to re-import.

Choosing the right tool

GoalUseDestroys real resource?Versioned in code
Rename / restructure addressmoved blockNoYes
Stop managing, keep resourceremoved block (destroy = false)NoYes
Stop managing and deleteremoved block (destroy = true)YesYes
Quick local address changeterraform state mvNoNo
Forget a resource imperativelyterraform state rmNoNo

Best Practices

  • Reach for declarative moved and removed blocks first; they are reviewed, repeatable, and self-documenting in pull requests.
  • Always inspect the plan after any refactor and confirm it reports 0 to destroy before applying.
  • Keep moved blocks 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 mv or state rm.
  • Use removed with destroy = false to hand ownership of a resource to another configuration or to retire Terraform management gracefully.
  • Refactor in small, isolated commits so a single moved or removed change is easy to review and roll back.
Last updated June 14, 2026
Was this helpful?