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 plan shows 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 to address and the id.
  • 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 with for_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.

  1. Write resource blocks that match the existing infrastructure as closely as you can. Use AWS console / aws ... describe-... to read attributes.
  2. Add an import block per resource, with the cloud provider’s identifier as id.
  3. 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.
  4. Iterate on the resource block until the plan shows zero in-place changes — only the import action.
  5. Apply. The resources are now in state with no infra changes.
  6. Optionally remove the import blocks 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 Importer per 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_each or count must be imported instance by instance — module.x.aws_iam_role.this["read"] — and the addresses can be tricky to construct. TF 1.7+ supports for_each inside an import block 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.

See also

References