Importing Existing Infrastructure
Import brings already-existing cloud resources under Terraform management without recreating them — the essential move when adopting Terraform on infrastructure that was built by hand. You point Terraform at a resource that already exists in AWS, and it records that object in state as if Terraform had created it.
Modern Terraform (1.5+) does this declaratively with import blocks that live in your config and show up in the plan, replacing the older one-off terraform import command that imported into state but left you to hand-write matching config from scratch.
Why Import Exists
Most real Terraform adoption is brownfield: a production VPC, an RDS instance, and a load balancer already exist, built through the console over the years, and you want Terraform to manage them going forward. The wrong answer is to delete and recreate them under Terraform — that is an outage and a data-loss risk on stateful resources. Import is the right answer: it adopts the live object into state, leaving the running infrastructure untouched.
import Blocks (1.5+)
An import block declares a target address and the ID of the existing object. You add it to your config, run plan to preview the adoption, and apply to bring the object into state. Because it is part of the config, the import shows up in the plan for review and can adopt many resources in a single apply — a reviewable, version-controlled operation rather than a sequence of manual commands.
import { to = aws_instance.web id = "i-0abc123def456" } resource "aws_instance" "web" { # config that matches the existing instance ami = "ami-0c55b159cbfafe1f0" instance_type = "t3.micro" }
Config Generation
Hand-writing config for a complex resource — every argument, every nested block — is where imports go wrong, because a single missed argument produces a destructive diff on first apply. Run terraform plan -generate-config-out=generated.tf with the import block in place and Terraform writes a starting config for the imported resource, derived from its real attributes. You then refine that generated file rather than building it from a blank page.
# with the import block present, generate matching config terraform plan -generate-config-out=generated.tf # then iterate plan until it reports no changes terraform plan
The Legacy terraform import Command
Before 1.5, the only tool was the imperative terraform import aws_instance.web i-0abc123 command. It imports the object into state but generates no config, leaving you to write a matching resource block by hand and discover the mismatches one destructive plan at a time. It handles one resource per invocation and never appears in a plan, so a reviewer cannot see what it did. It still works and you will meet it in older runbooks, but on any current version the import block is the better tool.
Verifying After Import
The proof an import succeeded is a clean plan. Iterate: import, plan, adjust the config to match whatever the plan wants to change, plan again — until Terraform reports no changes. A no-op plan means your config now describes the imported reality exactly, so the next apply will not touch the live resource. Apply before reaching that point and Terraform will "correct" production toward your incomplete config, which is precisely the outage import was meant to avoid.
import block — declarative, visible in the plan, supports config generation, and imports many resources in one reviewable apply. Choose it on any current Terraform version.
terraform import command — imperative, one resource per call, generates no config, and never shows in a plan. Reach for it only on a version older than 1.5 or a quick one-off where no review is needed.
- Importing into state but never reconciling the config, so the next plan wants to "fix" the resource and changes production.
- Hand-writing config for a complex imported resource and missing an argument, causing a destructive diff on first apply.
- Using the legacy CLI import for bulk adoption when import blocks would have done it declaratively and reviewably.
- Getting the import ID format wrong — some AWS resources use ARNs, some IDs, some composite keys — and hitting a confusing failure.
- Applying before the plan reports zero changes, letting Terraform reshape the live resource toward an incomplete config.
- Use
importblocks plus-generate-config-outto adopt existing resources, then refine the generated config. - Iterate plan until it reports no changes before applying, proving the config matches reality.
- Import in reviewable batches with the import blocks in version control, then remove them once the resources are in state.
- Check the resource's Registry page for the exact import ID format before importing.
- Adopt brownfield infrastructure with import rather than destroying and recreating it, especially anything stateful.
pulumi import with code generation
Ansible has no equivalent — being stateless, it has nothing to import into
Knowledge Check
Why import a hand-built resource instead of recreating it under Terraform?
- Import adopts the live object into state untouched, avoiding the outage and data loss recreation would cause
- Import is faster than recreation because it skips the plan phase entirely and applies the adoption in one step
- Recreating is impossible for any resource that was first built by hand in the AWS console
- Import automatically writes a complete, production-ready config that needs no further work
What advantage does the import block have over the legacy terraform import command?
- It is declarative and shows in the plan, supports config generation, and can import many resources in one apply
- It can create the underlying resource in AWS during apply when the object does not yet exist, unlike the legacy command
- It encrypts the imported resource's secrets in state so they never appear in plaintext
- It removes the need to ever write a matching resource block for the imported object
What does -generate-config-out do during an import?
- Writes a starting config for the imported resource derived from its real attributes, which you then refine
- Generates a destroy plan for the resource being imported so you can review the full teardown before it runs
- Exports the imported resource's secrets to a separate file kept outside of the state
- Produces the import ID automatically by querying AWS for the resource's name
How do you verify an import succeeded cleanly?
- Iterate plan until it reports no changes, proving the config matches the imported reality
- Apply immediately and check that the command returns with no error reported
- Confirm the imported resource's ID appears under the lineage field in state
- Run
terraform destroyand confirm it finds and reports the resource you just adopted into state
You got correct