Terraform

Terraform Refactoring Patterns: Practical Use of moved / removed / import

2026-04-19
NicheeLab Editorial Team

To clean up your configuration without breaking existing resources, you need the right tooling to keep state consistent. This article focuses on Terraform's moved, removed, and import, and shows how to sequence a safe refactor end to end.

Built on HashiCorp's stable official features, the article combines the points exams love to test with the real-world pitfalls you actually hit on the job. Version-dependent items are called out where relevant.

Why moved / removed / import?

Terraform applies diffs by reconciling configuration (code) against state. During a refactor, address changes, removals, and imports easily break the state-to-resource mapping, leading to accidental deletions or recreations. moved, removed, and import are the official mechanisms for repairing that mapping.

Specifically, moved handles address rewiring, removed declaratively signals removal, and import adopts existing assets. Used correctly, plan output becomes no-change or a minimal diff, dramatically improving the safety of maintenance work.

  • Principle: never break the state mapping. Fix the mapping first, then move the code.
  • A human always reads the plan. Stop immediately if unrelated recreations appear.
  • Exams frequently test the difference between each feature's effect on state versus on real infrastructure.
FeaturePrimary purposeEffect on state file / behavior
movedTell state about an address change (rename / move)Records the from to to mapping and prevents recreation. Plan is normally no-change.
removedDeclaratively signal resource removal and detach it safelyRemoves from state declaratively (use state rm when unsupported). Always verify the plan before destroying real resources.
import (block / CLI)Bring an existing resource under Terraform managementAdds the external resource to state. Plan is no-change if the configuration matches.

Flow: fix the state mapping first, then apply

Before                Mapping                 After
aws_s3_bucket.web  -> moved{from web to app} -> aws_s3_bucket.app
(not imported)     -> import{id=...}         -> state has instance
(legacy)           -> removed/from           -> state forgets legacy

Minimal example: building moved/removed/import into your workflow

# moved: rename safely
moved {
  from = aws_s3_bucket.web
  to   = aws_s3_bucket.app
}

# removed: declare removal (use state rm in unsupported / older environments)
# removed {
#   from = aws_s3_bucket.legacy
# }
# Alternative: terraform state rm aws_s3_bucket.legacy  # does not delete the real resource

# import (Terraform 1.5+): adopt an existing resource
import {
  to = aws_s3_bucket.logs
  id = "my-logs-bucket"
}

moved Block Basics and Standard Patterns

The moved block tells Terraform that a resource or module address has changed. With it, the state mapping is preserved and unnecessary destroy/create cycles are avoided.

Typical cases are renaming a resource, restructuring module hierarchy, or changing a for_each key. In every case the trick is writing the fully-qualified old address paired with the fully-qualified new address.

  • Placement: put it at the root of the module performing the change (it applies within that module).
  • Order: apply multiple moved blocks starting from the least dependent ones.
  • Explicitly include [key] / [index] for individual for_each / count instances.
  • For cross-module moves, write module.old.resource → module.new.resource.
PatternExample changeNotes
Simple renameaws_s3_bucket.web aws_s3_bucket.appAddress change within the same module only
Module movemodule.old.aws_s3_bucket.b module.new.aws_s3_bucket.bSpecify the boundaries of both modules precisely
for_each key changeaws_instance.srv["a"] aws_instance.srv["blue"]Provide a moved block for every key

Address remapping with moved

State (old)                  Mapping                        State (new)
module.web.aws_lb.app  --->  moved{from module.web...        module.edge.aws_lb.app
                               to   module.edge...}
resource.srv["a"]      --->  moved{from srv["a"] to srv["blue"]}  resource.srv["blue"]

Different ways to write moved

# 1) Simple rename
moved {
  from = aws_s3_bucket.web
  to   = aws_s3_bucket.app
}

# 2) Move between modules
moved {
  from = module.old.aws_security_group.sg
  to   = module.new.aws_security_group.sg
}

# 3) for_each key change
moved {
  from = aws_instance.srv["a"]
  to   = aws_instance.srv["blue"]
}

# Practical tip: add moved -> run plan and confirm no-change -> commit the rename/move code edits

Safe Removal Patterns with removed (and state rm)

When retiring a resource you no longer need, you must consciously control what stays and what goes in both state and real infrastructure. Terraform offers the removed block to declare removal in supported environments. Where it is not supported, terraform state rm is the fallback for detaching from state.

The key point: state rm only removes the resource from state and leaves real infrastructure intact. By contrast, deleting a resource from configuration alone produces a destroy plan. Which option you choose depends on whether your operational policy is to keep or delete the real resource.

  • Keep the resource: use state rm to detach it from Terraform management.
  • Delete the resource too: keep the configuration in place and let destroy run (confirm with tags or other final checks).
  • Prepare a PR and release note that spell out the order, and separate the apply timing into stages.
  • Mind version differences: prepare an alternative procedure when the removed block is unavailable.
GoalRecommended approachSide effects / notes
Delete the resource tooKeep the configuration, run destroy via plan/apply, then remove the codeReviewing the plan output is mandatory to prevent accidental deletion
Keep the resource (detach from management)terraform state rm <addr>Terraform no longer tracks it, and drift detection becomes impossible.
Declarative removalDeclare from in a removed block when supportedVerify environment compatibility (fall back to state rm when unsupported)

Decision tree for removal

Config remove?   State action         Infra action
Yes              plan shows -destroy  Destroy on apply
No (state rm)    state forgets        Infra remains
removed block    state forgets (decl) Infra: per plan/policy

Concrete removal procedures

# A) Delete the resource too: confirm -destroy in the plan, then apply
# (Keep the configuration in place for now)
resource "aws_s3_bucket" "tmp" { /* ... */ }
# -> If plan shows -/+, stop. Confirm it shows only - (delete).

# B) Keep the resource, detach from management: state rm
# Useful when handing the resource off to another team
terraform state list | grep aws_s3_bucket.tmp
terraform state rm aws_s3_bucket.tmp

# C) Declarative removal (when supported by your environment)
# removed {
#   from = aws_s3_bucket.legacy
# }

Declarative Import with the import Block

Since Terraform 1.5, the import block lets you declare which real resource (by ID) gets adopted at which address. If the configuration matches the resource's attributes, the plan stays no-change and you can adopt existing assets safely. The legacy terraform import CLI is still available too.

The official documentation for each resource lists the required import ID. For composite IDs (for example ARN, or project/region/name formats), make sure you specify the exact correct format.

  • The import block is added temporarily, and leaving it in place after the import is harmless.
  • Mismatches between configuration and the real resource show up as diffs in the plan, which helps isolate the cause.
  • List multiple instances using multiple import blocks.
  • CLI import is immediate but leaves no declaration behind, so the block form wins on historical traceability.
TargetExample import IDNotes
aws_s3_bucket.logsmy-logs-bucketGlobally unique bucket name
aws_iam_role.appapp-roleMay depend on account / path context
google_compute_instance.vm["blue"]projects/P/ zones/Z/ instances/NAMEConfirm the exact format

Declarative import flow

Existing Infra ----id----> import { to=addr, id=... } ----> State
                          ^ attributes must match resource config

Combining the import block with the CLI

# 1) Declarative import (recommended on 1.5+)
resource "aws_s3_bucket" "logs" {
  bucket = "my-logs-bucket"
  force_destroy = false
}
import {
  to = aws_s3_bucket.logs
  id = "my-logs-bucket"
}

# 2) The legacy CLI still works
# terraform import aws_s3_bucket.logs my-logs-bucket

Combining moved and import When Splitting or Merging Modules

When you split a monolithic module, or merge multiple modules, use moved to rewire addresses and use import to adopt resources that previously lived outside Terraform. Tackling it in stages keeps the plan stable.

The recommended approach is a staged rollout: 1) introduce the new module empty, 2) prepare the state mapping with moved, 3) move the configuration, 4) import the missing resources, 5) test.

  • Follow the order: prepare the mapping (moved) → move the configuration → import the missing pieces.
  • When splitting, cover every moved block using the full module.path notation.
  • Verify no-change plans in a test environment and apply in stages.
  • When merging, resolve naming collisions first (for example with prefixes).
StepMethodGoal / verification point
1. Introduce the new moduleAdd the module blockVerify dependencies and variable I/O consistency
2. Map the statemoved from old → newPlan must be no-change
3. Move the configurationMove resource definitions into the new moduleIf a diff appears, go back and revise moved
4. Adopt existing resourcesimport block / CLIAdopt unmanaged assets and converge to no-change

Staged rollout for a module split

module.monolith.aws_*  --moved-->  module.network.aws_* / module.app.aws_*
        ^                                   ^ import missing pieces here
        |                                   |
     initial                          after staged refactor

Skeleton for a module split

# Move SG from module.monolith into the new module.network
moved {
  from = module.monolith.aws_security_group.web
  to   = module.network.aws_security_group.web
}

module "network" {
  source = "./modules/network"
  # ... vars
}

# Import the existing resource that was missed
import {
  to = module.network.aws_network_acl.default
  id = "acl-123456"
}

Guardrails and CI: Codified Procedures That Do Not Break Things

Refactor quality is reproducibility of process. Standardize plan storage, review, separation of apply, and state backup. PRs containing moved or import in particular must share a no-change (or minimal-diff) plan as a screenshot or build artifact.

When drift is suspected, run a state health check (state list / show) before apply, and refresh if necessary. Targeted apply (-target) is a rescue tool, not something you run as part of normal operations.

  • terraform fmt/validate plan -out=plan.tfplan human review apply plan.tfplan
  • Enable state backups (remote backend with versioning).
  • Roll out critical changes in stages across environments (dev → staging → prod).
  • Prepare a rollback strategy for failures (restoring the previous state version).
Failure patternSymptomMitigation
Missing moved causes recreationplan shows -/+Re-check the address match and add the missing moved
Mismatched import IDPlan generates a large volume of changesRe-check the official documentation for the ID specification
Misuse of state rmResource is left behind and driftsClarify the operational policy (detach vs delete)

Minimal CI pipeline

[fmt] -> [validate] -> [plan -out] -> [manual review] -> [apply plan] -> [post-check]
   |                                                        ^ artifacts(plan, logs)

CI execution example (pseudo-code)

terraform fmt -check
terraform validate
terraform init -input=false
terraform plan -input=false -out=plan.tfplan
# After manual approval
terraform apply -input=false plan.tfplan
# Save the state backup / plan output for audit purposes

Check Your Understanding

Pro

問題 1

You want to rename aws_instance.web to aws_instance.app and also move it from module.old to module.new without causing recreation. Which procedure is most appropriate?

  1. Declare moved with from=module.old.aws_instance.web, to=module.new.aws_instance.app, confirm no-change in the plan, then move the configuration
  2. Rewrite the configuration first, take a plan, and if the diff is small enough, apply it
  3. Use terraform state rm to detach aws_instance.web, then apply the new app
  4. Use an import block to bring the existing web in under the app address

正解: A

Address changes (renames or module moves) are textbook cases for fixing the state mapping with moved first. Rewriting the configuration first or using state rm invites needless recreation or unwanted detachment. import is for bringing an unmanaged resource into Terraform management, not for renaming or moving resources that are already managed.

Frequently Asked Questions

What is the difference between moved and import? When should I use each?

moved tells the state about an address change for a resource that is already under Terraform management. import brings an existing resource that is not yet managed by Terraform into the state. Use moved for already-managed resources whose address changes, and import for unmanaged resources you want to adopt.

The removed block is not available in my environment. What should I do?

When removed is unsupported or constrained, branch by goal. If you also want to delete the real infrastructure, keep the configuration in place and let plan/apply run a destroy, then remove the code. If you want to keep the infrastructure but stop managing it, use terraform state rm.

How do I write moved for a for_each key change or a partial move?

Specify each instance explicitly. Example: moved { from = aws_instance.srv["a"] to = aws_instance.srv["blue"] }. Add one moved block per instance, and use fully-qualified addresses for cross-module moves the same way.

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.