Terraform

Terraform moved Block: Safe Refactoring Without State Loss

2026-04-19
NicheeLab Editorial Team

The moved block is a mechanism that declaratively reflects address changes (resource/module addresses) in state without recreating the underlying infrastructure.

Its biggest advantage is preventing procedural mistakes and environment drift during team development and CI/CD-driven refactoring.

When and Why to Use the moved Block

When you refactor and change resource names or module structure, the Terraform address changes too. Without intervention, plan shows a destroy for the old resource and a create for the new one. When you need to preserve existing resources and avoid downtime, declaring the old → new address mapping in a moved block lets apply rewire only the state binding while leaving the physical resource intact.

In practice, this comes up frequently with naming convention changes, modularization and monorepo restructuring, and migrations from count to for_each. Compared to manually editing state via commands, leaving the change history in code gives you better reproducibility, reviewability, and safety in CI.

  • Primary use cases: renaming resources, moving between modules, and changing instance index/key (such as count → for_each)
  • Not supported: moving data sources (data is not managed in state)
  • Benefits: avoiding destructive changes, team-wide reproducibility, and automatic application via plan/apply

Minimal example: renaming a resource (state moves only, the entity is unchanged)

resource "aws_s3_bucket" "app_primary" {
  # Old: aws_s3_bucket.app
  # Configuration is unchanged
}

moved {
  from = aws_s3_bucket.app
  to   = aws_s3_bucket.app_primary
}

Syntax, Addresses, and Placement Rules

The moved block is available in Terraform 1.1 and later. The syntax is moved { from = <old address> to = <new address> }. The from and to must point to state objects of the same kind (same resource type, same managed granularity). Moves across types or providers are not allowed.

Place the moved block in a module that can reference both the from and to addresses. A safe rule of thumb is to use the smallest common parent module of the two. For example, when moving a root-level resource into a child module, write the moved block in the root module — from points to the root resource, and to uses the module.<name>.<resource> form.

The preconditions: the to-side resource (or module) must exist in code as the new configuration but not yet be registered in state; the from-side object must exist in state but no longer exist in code (or no longer be referenced after the rename). It shows up in plan as a moved entry and is consumed exactly once during apply.

  • Placement principle: put it in the smallest common parent module that can reference both addresses
  • Type constraint: resource type and provider must remain the same (aws_s3_bucket → aws_s3_bucket is OK; aws_s3_bucket → aws_s3_object is not)
  • Not applicable: data blocks, output values, variables, and other non-state objects
  • State precondition: to is not yet in state; from is already registered in state
  • Output: appears as moved in plan; treated as a no-op after apply

Move accompanying modularization (root → child module)

# root module
module "storage" {
  source = "./modules/storage"
}

# Old: aws_s3_bucket.app (existed at root)
# New: module.storage.aws_s3_bucket.app (defined inside the child module)

moved {
  from = aws_s3_bucket.app
  to   = module.storage.aws_s3_bucket.app
}

How to Write moved for Typical Refactors

Here are the three most common patterns. In every case, first prepare the new definition (the to-side resource/module) in code, add the moved block at the same time, and confirm that plan shows only moved entries.

For count → for_each, every instance address changes, so you list as many moved blocks as needed. The key is to fix a one-to-one mapping from index numbers to key names.

  • Rename: a simple move that only changes the label
  • Module move: whether root ↔ child or child ↔ child, place moved in the smallest common parent
  • count → for_each: enumerate the old-index to new-key mapping with moved blocks

Visualizing the module move

Update binding onlyaws_s3_bucket.approot module (old)moved { from, to }Declaratively move statemodule.storage.aws_s3_bucket.appchild module (new)Terraform statePhysical resource is preservedUse a moved block to move state from root → child module (entity unchanged)

Examples of moved blocks by case

# 1) Rename a resource (within the same module)
resource "aws_s3_bucket" "app_primary" {}

moved {
  from = aws_s3_bucket.app
  to   = aws_s3_bucket.app_primary
}

# 2) Move from root → child module (declared at root)
module "storage" { source = "./modules/storage" }

moved {
  from = aws_iam_role.app
  to   = module.storage.aws_iam_role.app
}

# 3) count → for_each (map index → key individually)
# Old: aws_instance.web[count = 2]
# New: aws_instance.web[for_each = {"a" = ..., "b" = ...}]

moved {
  from = aws_instance.web[0]
  to   = aws_instance.web["a"]
}

moved {
  from = aws_instance.web[1]
  to   = aws_instance.web["b"]
}

Choosing Between moved and Other Options (Comparison Table)

Several operations resemble an address move. Pick based on intent. If you want to preserve existing resources and have the change reproducible across the team, moved is the first choice. For one-off emergency fixes or correcting historical drift, consider state commands. To pull in pre-existing external resources, import-family options are the right fit.

  • moved: declarative and recorded in history. Integrated into plan/apply and safe.
  • terraform state mv: immediate, manual, and one-shot. Leaves no history and is hard to reproduce across collaborators.
  • import (import block / terraform import): registers an existing entity into state — different intent from an address move.
  • replace (-replace flag or lifecycle): intentionally recreates the resource. Not a move.
MechanismNature of the OperationShareability / ReproducibilityImpact Scope
moved blockDeclarative; moves state exactly once during applyHigh (lives in code)Non-destructive (physical resource preserved)
terraform state mvImperative; edits state immediatelyLow (depends on manual operation)Non-destructive (but leaves room for human error)
import (import block / terraform import)Pulls existing resources into stateMedium (the block form can be codified)Non-destructive (intake only)
replace (-replace or lifecycle)Explicitly recreates the resourceMedium (mix of command and code)Destructive (recreation)

A Safe Migration Workflow (for Teams and CI)

Treat "adding the new definition" and "adding the moved block" as a single change, confirm that plan shows no create/destroy, then apply. Enable remote backend locking and roll out environment by environment.

When batching multiple moved blocks, verify in plan that every entry shows up as moved as expected. If the to-side already exists in state you will get an error, so resolve duplicates first (with state rm or destroy) before proceeding.

  • On a branch, add the new definition and the moved block together
  • Run terraform plan and confirm only moved entries appear
  • Apply in order: dev → staging → prod (backend locking is mandatory)
  • After every environment has been applied, delete the moved block (to avoid future noise)
  • In CI, gate on plan output containing no mixed destroy/create

Expected plan output (conceptual example)

Terraform will perform the following actions:

  # aws_s3_bucket.app_primary has moved to aws_s3_bucket.app_primary
    moved from aws_s3_bucket.app
          to aws_s3_bucket.app_primary

Plan: 0 to add, 0 to change, 0 to destroy. 2 to move.

Exam Prep: Common Topics and Pitfalls

Exams test you on choosing between moved, import, replace, and state mv; where to place the moved block; per-instance moves for count → for_each; the to/from preconditions; and what is not supported (data). It is also important to be able to read plan output (moved appears, no create/destroy).

Also remember: moved cannot be used across types or providers; apply fails if the to-side is already in state; and since moved is consumed once after apply, the recommended practice is to apply across every environment before deleting it.

  • Placement rule: place it in the smallest common parent module that can reference both addresses
  • Not supported: moving data blocks is unnecessary and not possible
  • count → for_each: write a moved block per instance (it is not handled automatically)
  • Type and provider must remain unchanged
  • The basic precondition: to exists in code but not in state; from exists in state but not in code
  • state mv is imperative; moved is declarative (moved is superior for team reproducibility)

Check Your Understanding

Pro

問題 1

You have aws_s3_bucket.web in the root module and want to move it to aws_s3_bucket.web_main inside the storage child module — without recreation. Which placement and definition of the moved block is correct?

  1. Place moved in the root module with from = aws_s3_bucket.web and to = module.storage.aws_s3_bucket.web_main
  2. Place moved in the storage child module with from = aws_s3_bucket.web and to = aws_s3_bucket.web_main
  3. Skip moved and run terraform state mv directly in production
  4. Place moved in any module with from = data.aws_s3_bucket.web and to = module.storage.aws_s3_bucket.web_main

正解: A

Place moved in the smallest common parent module that can reference both from and to — here that is the root. data is not eligible, and state mv is imperative rather than declarative, with low reproducibility.

Frequently Asked Questions

When can I safely delete a moved block?

Delete it once the moved block has been consumed in every target environment (dev, staging, prod, etc.) and the state move is complete. Leaving it in place for too long becomes noise, so cleaning it up at the end of the migration is the recommended practice.

The target address already exists in state and apply fails. What should I do?

This is a duplicate state condition. Resolve the duplication first — destroy the mistakenly created target object, or remove the unwanted state entry with terraform state rm, then re-apply the moved block.

I need dozens of moved blocks to migrate count to for_each. What is an efficient approach?

Prepare a mapping table from old indexes to new keys and generate the moved blocks via a template or script. Apply the changes in small, incremental PRs and verify at each step that plan shows only moved entries.

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
Terraform

HCL Syntax: Terraform's Configuration Language (2026)

HCL2 fundamentals for Terraform — blocks, attributes, expres...

Terraform

Terraform Authoring & Operations Pro: Complete Guide (2026)

Tactics for the Terraform Pro exam — module authoring, works...

Terraform

Terraform Providers: Plugin Management Fundamentals (2026)

Provider mechanics — required_providers, versions, mirrors, ...

Terraform

Terraform Resource Blocks: Declarative Infra Units (2026)

Resource block fundamentals — addresses, references, common ...

Terraform

Terraform Data Sources: Read-Only External Data (2026)

Data source basics — declaration, refresh behavior, dependen...

Browse all Terraform articles (102)
© 2026 NicheeLab All rights reserved.