cloud IaC terraform import claude-curated
Importing existing infrastructure into Terraform used to mean a one-shot terraform import CLI command that mutated state immediately and produced no reviewable diff. Terraform 1.5 introduced a declarative import block that fixes most of those ergonomics — imports happen during plan, are visible in PR review, and live in version control like any other resource definition. See Terraform CLI with Cloud Hybrid for the older CLI workflow.
CLI vs declarative import
terraform import aws_s3_bucket.example my-bucket
This works but has problems:
- The state mutation happens the moment the command runs — no plan, no review.
- The imported resource has no configuration yet; the next
planshows a huge diff between the discovered attributes and the empty resource block. - If you ran the command in the wrong workspace or environment, recovery is
state rm. - It doesn’t compose with code review or CI gates.
The declarative form moves the same operation into the config:
import {
to = aws_s3_bucket.example
id = "my-bucket"
}
resource "aws_s3_bucket" "example" {
bucket = "my-bucket"
# other attributes
}terraform plan reads the import block, fetches the live resource, and shows a plan that says “import this bucket and align it with this resource block”. Only on apply does state actually change. The import block can stay in the file or be removed in a follow-up commit; either way the resource is now under management.
Benefits
- Reviewable — the import shows up in a PR like any other change. A reviewer can sanity-check the
toaddress and theid. - Repeatable — re-running plan against an already-imported resource is a no-op; the block is idempotent.
- Version-controlled — the historical record shows when each resource was brought under management.
- Composable with
for_each— you can import many similar resources in one block by combining the import block withfor_each(TF 1.7+ adds first-class support; earlier versions need one block per resource). See Terraform Module Composition.
The removed block
Terraform 1.7 added a counterpart for the opposite operation:
removed {
from = aws_iam_role.legacy
lifecycle {
destroy = false
}
}This declaratively removes a resource from state without destroying it — the equivalent of terraform state rm, but reviewable and version-controlled. Use it when migrating a resource out of one root module into another, or when an out-of-band tool now owns the resource — a common cause of Terraform State Drift.
destroy = false is what distinguishes “remove from state, leave the infra alone” from a normal resource deletion.
Migration strategy: import without recreate
A typical scenario: a clickops-built environment needs to come under Terraform without an outage — a common step in adopting IaC.
- Write resource blocks that match the existing infrastructure as closely as you can. Use AWS console /
aws ... describe-...to read attributes. - Add an
importblock per resource, with the cloud provider’s identifier asid. - Run
terraform plan. The output will show “Terraform will perform the following actions: import” and then a diff between the live attributes and your written config. - Iterate on the resource block until the plan shows zero in-place changes — only the import action.
- Apply. The resources are now in state with no infra changes.
- Optionally remove the
importblocks in a follow-up commit; they’re only needed once.
The “iterate until diff is empty” loop is the bulk of the work. Pay attention to defaults the provider sets that you didn’t specify — they show up as no-op diffs and that’s fine, but a real diff means your block doesn’t match reality.
Limitations
- Not all resources support import. Each provider implements
Importerper resource type. Check the resource’s docs page for a usage example. AWS coverage is broad; some smaller providers leave gaps. The Cloudflare provider has notable gaps here. - Multi-instance resources are awkward. A resource with
for_eachorcountmust be imported instance by instance —module.x.aws_iam_role.this["read"]— and the addresses can be tricky to construct. TF 1.7+ supportsfor_eachinside animportblock which helps. - Computed-only attributes can mislead. Some attributes are populated by the provider after creation and cannot be set in config; they show up in state but not as plan diffs. This is normal but confusing on first contact.
- Cross-region or cross-account imports require the provider alias to be configured before the import block can target the right account.
- Sensitive data — imported state may contain attributes the provider considers sensitive (passwords, tokens). Handle the state file accordingly — see Cyber Security and consider Vault or Secrets Manager.
When CLI is still right
The declarative import block is preferable for almost every case, but the CLI command remains useful for:
- Exploratory imports during initial discovery (you don’t yet know if you’ll keep the resource under TF).
- One-off recovery during incident response, where the speed of a single command outweighs the loss of review.
- Scripting: if you’re generating thousands of import calls programmatically and emitting the resulting state via
terraform state pull, the CLI form is more amenable.
For everything that touches production through normal change-management channels, prefer the import block. See CI CD for Terraform and Terraform Cloud Workspaces for how this fits the broader pipeline; Terraform Cloud Pitfalls covers gotchas when running imports remotely.