Skip to main content

Refactoring: moved Blocks, import, State Manipulation

What This Concept Is

Three mechanisms for changing Terraform's idea of what it manages without destroying and recreating real infrastructure.

  • moved block (Terraform ≥ 1.1): declare in code that a resource has been renamed or relocated in the config, so Terraform updates state in place instead of proposing destroy+create.
  • import block (Terraform ≥ 1.5) or terraform import CLI: tell Terraform "this real-world resource already exists, adopt it into state at this address."
  • terraform state subcommands (mv, rm, list, show): low-level state manipulation. The power tools. Use with respect.

Refactoring in IaC is more fragile than in code because you are mutating an out-of-band state file that also tracks real infrastructure. The wrong move will destroy a database.

Why It Matters Here

Real systems rename things. A module graduates from modules/web to modules/services/web. An aws_instance.old_name becomes aws_instance.new_name because nomenclature improved. A security group created manually three years ago needs to be adopted into code.

Without moved and import, every such refactor shows up in the plan as -/+ -- destroy and recreate. For a stateless compute resource that might be fine. For a database, a DNS record, or an S3 bucket, that is catastrophic.

Concrete Example: moved

You rename a resource in code:

-resource "aws_instance" "app" {
+resource "aws_instance" "api" {
ami = "ami-0abc"
instance_type = "t3.small"
}

Without intervention, the next plan says:

Plan: 1 to add, 0 to change, 1 to destroy.

That destroys the instance and creates a new one with a new ID. Adding a moved block tells Terraform they are the same thing:

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

Now the plan reads:

# aws_instance.app has moved to aws_instance.api
Plan: 0 to add, 0 to change, 0 to destroy.

Commentary: the moved block is declarative refactoring. It updates state in place at apply time. You typically keep the block in the codebase for a release cycle (so old checkouts apply cleanly), then remove it in a follow-up PR. Removing it prematurely is a breaking change for anyone applying from an older state.

More moved patterns:

# Resource moved into a module:
moved {
from = aws_subnet.public[0]
to = module.vpc.aws_subnet.public[0]
}

# Module call renamed:
moved {
from = module.servers
to = module.fleet
}

# Resource changed from count to for_each:
moved {
from = aws_instance.web[0]
to = aws_instance.web["a"]
}

Each of these would, without moved, propose destruction. With moved, they are pure state relabeling.

Concrete Example: import

A colleague created an S3 bucket acme-legacy-logs by hand last year. You want to manage it from Terraform now.

Step 1: write the target resource block as it exists today:

resource "aws_s3_bucket" "legacy_logs" {
bucket = "acme-legacy-logs"
}

Step 2: add an import block pointing to the real resource's ID:

import {
to = aws_s3_bucket.legacy_logs
id = "acme-legacy-logs"
}

Step 3: run terraform plan. Expect:

Terraform will perform the following actions:

# aws_s3_bucket.legacy_logs will be imported
resource "aws_s3_bucket" "legacy_logs" {
bucket = "acme-legacy-logs"
...
}

Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.

Step 4: apply. Now Terraform manages the bucket. Remove the import block in a follow-up PR (it is idempotent but noisy to keep forever).

The tricky part is matching attributes. If the real bucket has versioning enabled and your config does not, the first plan after import will propose disabling versioning. Inspect the imported state (terraform state show aws_s3_bucket.legacy_logs) and update the config to match reality before applying any further changes.

Concrete Example: terraform state (Power Tools)

Before moved and import blocks existed, everyone did refactors via CLI:

# rename a resource in state
terraform state mv aws_instance.app aws_instance.api

# remove a resource from state (detach; real resource still exists)
terraform state rm aws_s3_bucket.temp

# list everything in state
terraform state list

# inspect one resource
terraform state show module.vpc.aws_vpc.this

These commands are still necessary when:

  • you are on pre-1.1 Terraform (older legacy repos)
  • you need to detach a resource from state without destroying it
  • you are untangling a state corruption

Guardrails: any terraform state command with mv or rm in it is a state mutation that bypasses plan review. Back up state before (terraform state pull > backup.tfstate), do it in a controlled environment, and commit a plan that shows "no changes" afterward as evidence that state now matches config.

Common Confusion / Misconception

"moved also imports a resource." It does not. moved relabels a resource already in state. To adopt an unmanaged resource, use import.

"I can moved from one resource type to another." No. moved only handles address changes, not type changes (aws_instance -> aws_spot_instance is a destroy+create, period).

"terraform import destroys the resource." It does not. It updates state to reference the existing resource. The resource itself is untouched.

"State commands are deprecated because of moved/import blocks." They are not. Blocks cover the common cases; the CLI covers the edge cases (state removal, state manipulation during corruption recovery). Both tools stay in the toolbox.

How To Use It

  1. Default to moved blocks for any rename or relocation. They are reviewable (visible in the PR) and reversible.
  2. Default to import blocks for adoption. Write the resource block first, then the import block.
  3. Reserve terraform state mv/rm for scenarios the blocks cannot handle or for pre-1.1 Terraform.
  4. Back up state before any CLI state manipulation: terraform state pull > state.backup.<timestamp>.json.
  5. After any refactor, confirm terraform plan shows "No changes" against the same code. That is the proof.

Check Yourself

  1. When would you reach for terraform state mv instead of a moved block?
  2. You add an import block, plan, and see the plan also proposes destructive updates. What do you do before applying?
  3. Why should a moved block stay in the codebase for a release cycle before being deleted?

Mini Drill or Application

In your sandbox:

  1. Create a resource, apply, and confirm it works.
  2. Rename it in code without a moved block. Plan -- observe the destroy+create.
  3. Add a moved block. Plan again -- observe "No changes."
  4. Apply. Confirm the real resource was not recreated (check its ID/ARN).

Repeat the drill with import: create a resource by hand in the cloud console, then adopt it into Terraform with an import block.

See also (external)


Source Backbone

Infrastructure-as-code details are tool-specific, but these local books provide the operational backbone for shell, Git, and change discipline.