Importing Existing Infrastructure Into Terraform at Scale
Bringing a pile of click-ops resources under Terraform without an outage is a real project. Here's a staged approach using import blocks, generated config, and zero-change plans.
- #terraform
- #import
- #brownfield
- #migration
- #iac
- #state
Greenfield Terraform is easy. The hard project is the brownfield one: a few years of click-ops, CloudFormation, and “I’ll Terraform it later” has left you with hundreds of live resources that Terraform doesn’t know about. Now leadership wants it all under code, and the catch is obvious — every step has to happen without breaking the production those resources are running.
I’ve done this migration on environments with hundreds of resources. The good news is that modern Terraform makes it far less painful than the old terraform import one-at-a-time grind. The discipline that keeps it safe is simple: every step ends with a plan that shows zero changes. Here’s the staged approach.
The cardinal rule: the goal is a no-op plan
Importing a resource means telling Terraform “this live thing now maps to this config.” If your config doesn’t match the live resource exactly, the next plan wants to “fix” the difference — which on real infrastructure can mean replacing a database or recreating a load balancer. So the success criterion for every import is not “it imported,” it’s “terraform plan afterward shows no changes.” Until you see that empty plan, you’re not done.
Stage 1: import blocks, not the CLI
The old way was terraform import aws_instance.web i-1234, run once per resource, with no record in your code. The modern way is the import block — declarative, reviewable, and committed to the repo:
import {
to = aws_instance.web
id = "i-0abc123def456"
}
You add these blocks, run terraform plan, and Terraform tells you what it would import and whether your config matches. Because the blocks live in code, the whole import is a reviewable pull request instead of a pile of untracked CLI runs. After a successful apply, the blocks have done their job and you delete them.
Stage 2: generate the config you don’t want to hand-write
The tedious part of import was always writing HCL to match each resource attribute-by-attribute. Terraform can now generate that config for you. Point an import block at a resource and ask Terraform to write the config:
terraform plan -generate-config-out=generated.tf
For every import block whose target resource doesn’t yet exist in config, Terraform writes a best-effort resource definition into generated.tf. It’s not perfect — it includes read-only attributes you’ll need to strip, and it won’t know your variables or modules — but it turns “write 80 lines of HCL from the AWS console” into “review and clean up 80 generated lines.” That’s the difference between a feasible migration and one nobody finishes.
The workflow per batch:
- Write
importblocks for a batch of resources. - Run
terraform plan -generate-config-out=generated.tf. - Move and clean the generated resources into your real files (remove computed attributes, parameterize).
- Run
terraform planuntil it shows import-only, then zero changes. - Apply, then delete the import blocks.
Stage 3: batch by blast radius, not by convenience
Don’t import everything in one heroic PR. Batch the work, and order the batches by how dangerous a mistake would be:
- First: stateless, low-risk resources — security groups, IAM policies, S3 buckets, DNS records. A botched import here is cheap to fix.
- Next: networking — VPCs, subnets, route tables. Higher coupling, still rarely destructive on import.
- Last: the stateful crown jewels — RDS instances, EBS volumes, anything that replaces rather than updates. By the time you reach these you’ve built the muscle, and you triple-check the zero-change plan before applying.
Within a batch, keep the count small enough that you can actually read the plan. A plan with 200 changes is a plan nobody reads carefully.
Finding the IDs at scale
Hand-collecting resource IDs from the console doesn’t scale past a handful. Two approaches that do:
- Query the cloud API and template the import blocks.
aws ec2 describe-instances --query '...'piped into a small script that emitsimport { to = ... id = ... }blocks gets you the bulk of a batch in seconds. - Tooling like
terraformerreverse-engineers existing infrastructure into both config and state. It’s a blunt instrument — the output needs heavy cleanup and it doesn’t understand your module structure — but as a starting inventory of what exists and what its IDs are, it saves a lot of clicking.
Whatever you use, treat generated artifacts as a draft. The migration’s quality is decided in the cleanup, not the generation.
The drift trap
Brownfield resources have usually drifted from any sane baseline — someone bumped a setting in the console six months ago and never told anyone. When you import and the plan shows changes, stop and figure out why before you make the plan green. Two valid resolutions:
- The live state is correct: update your config to match it. The console value wins.
- The live state is wrong: leave your config as the intended value and let the first real apply correct the drift — but do this deliberately, knowing it will change live infrastructure, never by accident.
The mistake to avoid is blindly editing config to silence the plan without understanding which direction the truth lies. That’s how an import quietly reverts a production fix.
Lock it down once it’s in
The moment a resource is under Terraform, close the console door. Tighten IAM so humans can’t mutate imported resources out-of-band, and route all future changes through the same pipeline. An import migration that doesn’t end with “no more click-ops on these resources” just creates a new, more confusing source of drift.
The payoff
Done in stages — import blocks in PRs, generated config as a draft, batches ordered by blast radius, and a zero-change plan as the gate on every step — a brownfield migration is methodical instead of terrifying. You convert your infrastructure from “whatever’s in the console” to “whatever’s in the repo,” and from there everything else in your Terraform toolkit finally applies.
Import PRs are exactly where a careful reviewer catches the resource whose config doesn’t quite match live state before it triggers a replacement. That second pass is what our AI code review tooling is built to provide. For more brownfield and state patterns, see the full Terraform guides.
Examples are illustrative. Never trust an import until terraform plan shows zero changes, and validate stateful imports with extra care.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.