A Terraform moved block renames a resource, and the plan shows a move instead of a destroy and create.
Terraform & OpenTofu

Terraform moved, removed, and import Blocks: Refactor Without Recreating Resources

Renaming a resource or moving it into a module makes Terraform plan a destroy and create. The moved, removed, and import blocks fix that in code, with a plan you can review.

Reviewed for technical accuracy: October 14, 2026

  • Changing a resource address makes Terraform plan a destroy and create
  • moved updates the address in state; removed stops managing with or without destroying; import adopts existing objects
  • All three are reviewed in a plan, unlike terraform state mv and state rm
  • Keep moved blocks in shared modules; use ignore_changes only for attributes another system owns

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:

HCL
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:

HCL
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:

HCL
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:

HCL
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:

HCL
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 moved block for every address that changes, and a removed block 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 moved blocks 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.

Quick answers

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.

Sources
From article to workflow

Turn infrastructure context into a reviewed change.

Review generated infrastructure code with policy checks, cost context, approval, and audit evidence before deployment.

Related articles

All articles