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.
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.
| Feature | Primary purpose | Effect on state file / behavior |
|---|---|---|
| moved | Tell state about an address change (rename / move) | Records the from to to mapping and prevents recreation. Plan is normally no-change. |
| removed | Declaratively signal resource removal and detach it safely | Removes 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 management | Adds 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 legacyMinimal 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"
}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.
| Pattern | Example change | Notes |
|---|---|---|
| Simple rename | aws_s3_bucket.web → aws_s3_bucket.app | Address change within the same module only |
| Module move | module.old.aws_s3_bucket.b → module.new.aws_s3_bucket.b | Specify the boundaries of both modules precisely |
| for_each key change | aws_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 editsWhen 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.
| Goal | Recommended approach | Side effects / notes |
|---|---|---|
| Delete the resource too | Keep the configuration, run destroy via plan/apply, then remove the code | Reviewing 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 removal | Declare from in a removed block when supported | Verify 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/policyConcrete 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
# }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.
| Target | Example import ID | Notes |
|---|---|---|
| aws_s3_bucket.logs | my-logs-bucket | Globally unique bucket name |
| aws_iam_role.app | app-role | May depend on account / path context |
| google_compute_instance.vm["blue"] | projects/P/ zones/Z/ instances/NAME | Confirm the exact format |
Declarative import flow
Existing Infra ----id----> import { to=addr, id=... } ----> State
^ attributes must match resource configCombining 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-bucketWhen 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.
| Step | Method | Goal / verification point |
|---|---|---|
| 1. Introduce the new module | Add the module block | Verify dependencies and variable I/O consistency |
| 2. Map the state | moved from old → new | Plan must be no-change |
| 3. Move the configuration | Move resource definitions into the new module | If a diff appears, go back and revise moved |
| 4. Adopt existing resources | import block / CLI | Adopt 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 refactorSkeleton 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"
}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.
| Failure pattern | Symptom | Mitigation |
|---|---|---|
| Missing moved causes recreation | plan shows -/+ | Re-check the address match and add the missing moved |
| Mismatched import ID | Plan generates a large volume of changes | Re-check the official documentation for the ID specification |
| Misuse of state rm | Resource is left behind and drifts | Clarify 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 purposesPro
問題 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?
正解: 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.
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.
Practice with certification-focused question sets
無料で問題を解いてみる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.
HCL Syntax: Terraform's Configuration Language (2026)
HCL2 fundamentals for Terraform — blocks, attributes, expres...
Terraform Authoring & Operations Pro: Complete Guide (2026)
Tactics for the Terraform Pro exam — module authoring, works...
Terraform Providers: Plugin Management Fundamentals (2026)
Provider mechanics — required_providers, versions, mirrors, ...
Terraform Resource Blocks: Declarative Infra Units (2026)
Resource block fundamentals — addresses, references, common ...
Terraform Data Sources: Read-Only External Data (2026)
Data source basics — declaration, refresh behavior, dependen...