Skip to main content
RunBook Academy

TerraformX · State Operations: Read, Move, Remove, ImportProduction Terraform

moved Blocks: Declarative Refactoring

Intermediate⏱ ~12 minbash

What you'll learn

  • Use the moved block to declare that a resource address has changed without destroy/recreate
  • Distinguish the moved block (declarative, reviewable) from state mv (imperative)
  • Apply the discipline of separate PRs for moved blocks and the underlying refactor
  • Remove the moved block after a successful apply

Prerequisites

None — start here.

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13

Not yet marked complete on this device.

terraform state mv is the imperative fix for a changed address: run it once, log it, move on. The moved block is the declarative fix: declare it in code, review it in the same pull request as the configuration change, apply it through the normal plan/apply flow. For any refactor that ships in a pull request, the moved block is the production tool.

When moved blocks apply

Terraform 1.1 introduced the moved block. It tells Terraform that a resource address has changed; the real-world resource has moved from the old address to the new one; the state should be rewritten to match.

Without moved block:
   Old config: resource "aws_instance" "web"
   New config: resource "aws_instance" "app"
   Plan:       destroy aws_instance.web, create aws_instance.app

With moved block:
   moved { from = aws_instance.web, to = aws_instance.app }
   Plan:       (first apply) Move aws_instance.web to aws_instance.app
               (subsequent plans) No changes

The moved block sits in any .tf file (commonly alongside the new resource block). On the next plan, Terraform detects the move and rewrites the state. After a successful apply, the block can be removed.

The moved block syntax

moved {
  from = aws_instance.web
  to   = aws_instance.app
}

The from and to are resource addresses. They must be valid addresses in the configuration (the destination block must exist).

For a move into a module:

moved {
  from = aws_vpc.main
  to   = module.network.aws_vpc.main
}

For a move between modules:

moved {
  from = module.network.aws_subnet.public
  to   = module.network_v2.aws_subnet.public
}

For multiple moves (one block per resource):

moved {
  from = aws_subnet.public[0]
  to   = aws_subnet.public["us-east-1a"]
}

moved {
  from = aws_subnet.public[1]
  to   = aws_subnet.public["us-east-1b"]
}

moved {
  from = aws_subnet.public[2]
  to   = aws_subnet.public["us-east-1c"]
}

The procedure: separate PRs

The production discipline for a rename is two pull requests, not one:

PR 1: the moved block only.

+ moved {
+   from = aws_instance.web
+   to   = aws_instance.app
+ }

  resource "aws_instance" "web" {
    ...
  }

(The destination block does not yet exist. The moved block alone is valid because moved accepts addresses not yet in configuration as the destination, as long as the source exists in state.)

terraform plan
# aws_instance.web: Refreshing state... [id=i-0abc...]
# aws_instance.web: Move to aws_instance.app
# Plan: 0 to add, 0 to change, 0 to destroy.

terraform apply
# Move complete.

After apply, state now records the resource at the new address aws_instance.app. The configuration block for aws_instance.web still exists. The next plan would propose to destroy aws_instance.web from the cloud’s perspective — except the resource is no longer in state under that address. This is the danger of PR 1: if a follow-up PR removes the web block without first adding the app block, the apply will destroy the real-world resource.

The discipline: PR 1 adds the moved block AND the new resource block. The moved block alone is too easy to misread.

PR 2: the rename.

- resource "aws_instance" "web" {
+ resource "aws_instance" "app" {
    ...
  }

(With the moved block from PR 1 in place.)

terraform plan
# No changes. Your infrastructure matches the configuration.

The app block already exists in configuration (added in PR 1). The state already points at the new address (moved by PR 1). The plan sees no diff. Apply; no changes. Remove the moved block in a follow-up PR (or in the same PR if the team prefers a single change).

Multiple moves: the count-to-for_each case

A team is migrating a count = 3 resource to for_each with stable string keys. Each instance needs its own moved block.

Before:

resource "aws_subnet" "public" {
  count = 3
  vpc_id = aws_vpc.main.id
  cidr_block = "10.0.${count.index}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]
}

After:

resource "aws_subnet" "public" {
  for_each = toset(["us-east-1a", "us-east-1b", "us-east-1c"])
  vpc_id = aws_vpc.main.id
  cidr_block = "10.0.${index(local.az_list, each.value)}.0/24"
  availability_zone = each.value
}

moved {
  from = aws_subnet.public[0]
  to   = aws_subnet.public["us-east-1a"]
}

moved {
  from = aws_subnet.public[1]
  to   = aws_subnet.public["us-east-1b"]
}

moved {
  from = aws_subnet.public[2]
  to   = aws_subnet.public["us-east-1c"]
}

The plan shows three moves, one per instance. Apply. Each instance is rewritten in state; no real-world change.

When moved blocks are not enough

moved rewrites the address. It does not change resource types or attributes. For:

  • Different provider: use state replace-provider.
  • Different resource type: use state rm + import.
  • Different module structure with renames: the moved block handles this case; no state mv is needed.

Removing the moved block

After a successful apply, the moved block is no longer needed. Terraform does not error on a moved block that has already been applied (the move is one-shot), but a stale moved block in code is noise. Remove it in a follow-up commit:

- moved {
-   from = aws_instance.web
-   to   = aws_instance.app
- }
terraform plan
# No changes. (moved block removal is not a real change.)

Validation

READ-ONLY

# Confirm the plan reflects the moved block
terraform plan
# First apply: "Move aws_instance.web to aws_instance.app"
# Subsequent: "No changes."

# Confirm the new address is in state
terraform state list | grep 'aws_instance.app'
# aws_instance.app

# Confirm the old address is not in state
terraform state list | grep 'aws_instance.web'
# (empty)

Production failure modes

Symptom: “Error: moved block refers to a resource not present in the configuration”. Cause: the source or destination address is spelled wrong, or the destination block was not added. Verify both addresses exist in configuration.

Symptom: the moved block has no effect; the plan still proposes destroy-and-create. Cause: the moved block syntax is wrong (missing from or to), or the addresses refer to the wrong resources. Inspect the plan carefully.

Symptom: the moved block produces a plan to move the resource twice. Cause: a moved block from a previous PR is still in code alongside the new one. Remove the stale block.

Symptom: lock acquire failed during apply of the moved block. Cause: another apply is in progress. Wait or break the lock with team agreement.

Symptom: after the moved-block apply, the next plan proposes to destroy the old address. Cause: the new resource block was not added in the same PR. Add the destination block; re-plan; the move should already be complete in state.

Recovery

If a moved block produces unexpected behaviour, the recovery is:

  1. Remove the moved block from code.
  2. terraform plan; expect the original behaviour (destroy + create if the addresses differ).
  3. Either revert the configuration change, or add a state mv to rewrite the state imperatively.

What comes next

The next part covers state security: the threat model, the access controls, the encryption, and the incident response for the state file.

Verification

  • You can write a moved block that declares an address change.
  • You can describe the two-PR discipline: moved block plus new block first, then remove the old block.
  • You can spot a stale moved block that has already been applied.
  • You can recover from a wrong moved block by removing it and planning again.

Knowledge check · 7 questions

  1. Q1. When was the moved block introduced?

  2. Q2. What is the production discipline for shipping a moved block?

  3. Q3. After a successful apply, the moved block must be removed from code immediately or the next apply will fail.

  4. Q4. Which is the right tool for migrating a `count = 3` resource to `for_each` with stable string keys?

  5. Q5. Which of these are valid uses for the moved block? (Select all that apply.)

  6. Q6. A team adds a moved block but the plan still proposes to destroy the old address and create the new one. What is the most likely cause?

  7. Q7. A team renames aws_db_instance.primary to aws_db_instance.main in a configuration PR. They want the change to be reviewable. Which is the right workflow?

Passing score: 75%. Answers are checked in this browser.