Brownfield Adoption at Scale
Almost nobody starts with Terraform. Teams start with years of console-built infrastructure — a live estate that someone clicked into existence and that now runs production — and have to bring it under management without downtime and without a big-bang rewrite. Brownfield adoption is the strategy for that migration: importing existing infrastructure in safe increments, drawing state boundaries as you go, and accepting that this is a phased program measured in months, not a weekend project.
This is the capstone of the course because it pulls together everything before it. Import brings resources under management, modules and state organization decide where they live, and the core workflow verifies each step. Adoption is those tools applied to a real, running estate that you are not allowed to break.
The Brownfield Reality
The defining constraint is that recreation is off the table. You cannot destroy production and let Terraform rebuild it — the resources exist, they hold state and traffic, and a rebuild means an outage and risk nobody will sign off on. So adoption is import, not creation: you bring the resources Terraform didn't make under its management exactly as they are. The estate is also messy in ways greenfield work never is — undocumented dependencies, resources nobody remembers creating, and configurations that don't match any standard. The plan has to survive contact with that mess.
Incremental Import Strategy
The unit of adoption is a small, reviewable batch. Use import blocks to declare which existing resources to bring under management, lean on -generate-config-out to get a starting configuration rather than hand-writing every argument, and then iterate the configuration until plan reports no changes. That no-change plan is the proof: it means the configuration now matches reality exactly, so the first real apply won't touch anything. A batch isn't done until its plan is clean.
# declare the existing resources to adopt — reviewable in the plan import { to = aws_vpc.main id = "vpc-0abc123" } import { to = aws_subnet.app id = "subnet-0def456" } # generate a starting config, then refine until plan is clean: # terraform plan -generate-config-out=generated.tf # ...edit until "No changes. Your infrastructure matches the configuration."
The import blocks live in version control and show up in the plan, so a batch is a reviewed change like any other. Once a batch is imported and verified, the import blocks have done their job and can be removed. Then you move to the next batch — the estate comes under management one safe increment at a time.
Drawing State Boundaries During Migration
Import forces a decision you can't defer: which state does each resource go into. The temptation is to dump everything into one giant state to get it over with, and that bakes in the worst possible structure — a single high-blast-radius state where any change risks everything and every apply is slow. Draw boundaries as you import instead, aligned to blast radius and ownership: separate state for networking, for the data tier, for each application, for each environment. The migration is the one cheap chance to get these boundaries right, because moving a resource between states later is surgery you'd rather not do.
Prioritization and Sequencing
Not everything needs importing at once, and the order matters. Bring the high-churn, high-risk infrastructure under management first — the resources that change often, where drift and undocumented manual edits cause the most pain, and where having a reviewed plan delivers the most value. Stable, rarely-touched resources can wait; there's little benefit to rushing a thing nobody ever changes under management. Sequencing by risk and churn means the migration pays back from the earliest batches instead of after the whole estate is done.
Organizational Change
The hardest part isn't technical. Bringing infrastructure under Terraform but letting teams keep changing it in the console guarantees constant drift — every manual edit is a change Terraform doesn't know about, and the next plan either reverts it or fights it. Adoption only sticks when teams stop using the console on managed resources and route every change through code. That is a habit and a governance change, not a Terraform feature, and it's the difference between adoption that holds and an estate that drifts back to where it started. Pair the technical migration with the organizational one or the technical work erodes.
That organizational discipline is also where the whole course lands. Everything from the first plan to this last page rests on a single commitment: that the code is the source of truth, and reality is made to match it — not the other way around.
- Attempting a big-bang import of an entire estate at once instead of safe, reviewable increments, so a single mistake risks everything.
- Importing resources but never reconciling the configuration to a clean no-change plan, so the first apply changes production.
- Importing everything into one giant state during migration, baking in a high-blast-radius structure that's painful to break apart later.
- Bringing infrastructure under Terraform but letting teams keep changing it in the console, guaranteeing constant drift.
- Importing the stable, never-touched resources first while the high-churn infrastructure that actually causes pain stays unmanaged.
- Adopt incrementally: import in reviewable batches, each verified to a no-change plan before moving to the next.
- Draw sensible state boundaries during the migration, aligned to blast radius and ownership, rather than dumping everything into one state.
- Sequence by risk and churn — bring the volatile, high-stakes infrastructure under management first.
- Use
importblocks with-generate-config-outand refine until the plan is clean, then remove the import blocks. - Pair the technical adoption with the organizational change of stopping console edits on managed resources, so adoption holds.
Knowledge Check
Why must brownfield adoption be incremental rather than a big-bang migration?
- Importing in small reviewable batches keeps any one mistake contained, on a live estate where recreation isn't an option
- Terraform technically can't import more than one resource per apply, so each apply is capped at a single addr
- AWS rate-limits imports to a fixed number per day, and exceeding that quota blocks the remainder of the batch until the next day
- Incremental import is required to generate the state file at all
How do you verify a batch of imported resources is safe to apply?
- Refine the configuration until plan reports no changes, proving it matches reality so the first apply touches nothing
- Run the first apply immediately against production and roll back from a state backup if anything in the live estate breaks
- Delete the live resources and let Terraform recreate them from scratch to confirm the config produces a match
- Check that the state file is larger than before the import
Why do state boundaries matter during the migration specifically?
- One giant state bakes in a high-blast-radius structure, and the import is the cheap chance to draw boundaries before relocating resources later becomes surgery
- Multiple states are required for Terraform to perform any import at all, since each import block must target its own separate backend, workspace, and provider alias
- Boundaries only matter after adoption is complete, not during
- A single state imports faster and is always the right choice
What organizational change does lasting adoption require?
- Teams stop changing managed resources in the console and route every change through code, or the estate drifts back
- A second paid Terraform license purchased for each application team that touches any part of the newly imported estate
- Consolidating all infrastructure into a single AWS account so one state can manage the whole estate
- Disabling the AWS console entirely for all users
You got correct