We inherited 200-odd AWS resources built by hand over four years, with no state file anywhere. Here's how import blocks and a generation workflow got them under Terraform without a rebuild.
The handover doc for the account we inherited was one sentence: "It's all in AWS, ask Dmitri." Dmitri had left. There were roughly 200 resources, VPCs, security groups, RDS instances, a tangle of IAM roles, all clicked together in the console over four years. No Terraform, no CloudFormation, no state anywhere. The first time someone needed to change a security group rule, they did it in the console, because that was the only option, and that's exactly how the mess had grown in the first place.
Rebuilding from scratch was off the table. These were live production resources with data and traffic. We had to import what existed, in place, without recreating anything.
The classic terraform import command imports one resource into state but writes no configuration. You still have to hand-write the matching HCL, and if your HCL doesn't exactly match reality, the next plan wants to change or destroy the resource. For 200 resources, hand-writing HCL that matches four years of console tweaks is a week of soul-destroying work and a high chance of a plan that proposes to delete your production database.
Terraform 1.5+ import blocks changed this. You declare the import in config, and Terraform can generate the HCL for you:
import {
to = aws_db_instance.primary
id = "devopsness-prod-db"
}
import {
to = aws_security_group.api
id = "sg-0a1b2c3d4e5f6a7b8"
}
Then generate the configuration instead of writing it by hand:
terraform plan -generate-config-out=generated.tf
Terraform reads the live resource and writes HCL that matches its current state into generated.tf. That flips the work from "write perfect HCL from memory" to "review generated HCL and clean it up," which is an order of magnitude faster and far less error-prone.
You don't want 200 import blocks in one file and one giant plan. We batched by resource type and blast radius. Networking first (VPCs, subnets, route tables), because everything depends on it. Then security groups. Then the stateful stuff (RDS, ElastiCache) last, because those are the resources where a mistaken -replace is catastrophic.
To find what actually exists, we didn't trust the console. We queried the API and generated import blocks programmatically:
aws ec2 describe-security-groups \
--query 'SecurityGroups[].[GroupId,GroupName]' \
--output text | while read id name; do
printf 'import {\n to = aws_security_group.%s\n id = "%s"\n}\n' \
"$(echo "$name" | tr -c 'a-zA-Z0-9' '_')" "$id"
done > sg_imports.tf
That gave us a complete, accurate list, no relying on a stale runbook. For a first pass across everything, terraformer can bulk-generate both state and config, but its output is messy and we found it better for discovery than as a final artifact.
-generate-config-out gives you working HCL, not good HCL. Two problems show up every time.
First, it hardcodes everything. IDs, ARNs, CIDR blocks are all literals. A generated security group references vpc-0abc123 as a string instead of aws_vpc.main.id. You have to go through and wire up the references so the resources actually depend on each other in the graph, otherwise Terraform doesn't know the SG belongs to the VPC and can't order operations correctly.
Second, it captures drift as if it were intent. If someone had manually set an odd apply_immediately or left a default tag off, the generated config bakes that in. We diffed the generated config against our conventions and normalized tags, naming, and lifecycle rules as we went.
The check that everything imported correctly is a clean plan:
terraform plan
# No changes. Your infrastructure matches the configuration.
That line, "No changes," is the whole goal. It means state, config, and reality agree. Until you see it, you're not done, and any resource still showing a diff is one that Terraform would alter or destroy on the next apply.
Import populates state but does not populate depends_on or data-source relationships you'd normally write by hand. An RDS instance imported cleanly, plan was green, but its subnet group had been imported separately and the implicit dependency wasn't expressed. A later refactor reordered things and Terraform tried to modify the subnet group in a way that would have forced the database to be recreated. We caught it in plan review, not apply, which is the entire argument for reading every plan line by line during a migration like this. terraform plan output during an import project is not a formality. It's the safety net.
Use import blocks with -generate-config-out, not the legacy terraform import command, for anything past a handful of resources. Generate the import blocks from the AWS API rather than a runbook so your list is real. Batch by dependency order, network first and stateful resources last, and treat the generated HCL as a draft to clean up, not a finished product. Above all, do not apply until terraform plan reports no changes, and read every plan during the migration like the database's life depends on it, because sometimes it does.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Static service tokens leaked into logs and never rotated. SPIFFE identities plus SPIRE-issued SVIDs gave us short-lived certs and killed the shared-secret sprawl.
We moved 40 services off the nginx Ingress controller onto Gateway API without a single dropped connection. Here's the routing overlap trick that made it boring.
Explore more articles in this category
The Backstage demo always wows leadership. Then six months later the catalog has 400 stale entries and nobody trusts it. Here's what got ours to actually stick.
A single ALTER TABLE took a lock and stalled every write for 40 seconds during peak traffic. Expand-contract is how we stopped shipping outages.
Adding a read replica cut primary load 60%, then support tickets rolled in about users not seeing their own edits. Replication lag turned into a correctness bug we had to route around.
Evergreen posts worth revisiting.