Chapter 13: Advanced Patterns
Topic 80

Brownfield Adoption at Scale

AdoptionMigration

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.

One incremental adoption batch
import a batch
reconcile to a no-change plan
next batch

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.

import a batch declaratively, then verify to a no-change plan
# 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.

Common Mistakes
  • 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.
Best Practices
  • 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 import blocks with -generate-config-out and 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.
Comparable tools CloudFormation resource import and the AWS Migration Hub Pulumi import with code generation former2 generates IaC from existing AWS resources to bootstrap adoption

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