Terraform-Driven AWS Migrations: A Playbook From the Field

July 25, 2026

An AWS migration goes well when it's boring. Boring cutovers happen when Terraform is the source of truth from day one — not bolted on after Landing Zone accounts are already hand-crafted.

This is the playbook I use for enterprise migrations, distilled from Rogers Bank, ExxonMobil, DNB Bank, and Adecco engagements.

Phase 1: Inventory and wave planning

Before any Terraform is written, you need three artefacts:

1. An honest inventory. Not what the CMDB says — what actually exists. Discovery via AWS Application Migration Service (MGN), Migration Evaluator, or an agent-based scan. Cross-reference with billing to catch orphaned resources.

2. A dependency graph. Applications, databases, integrations, batch jobs, service accounts. Anything that talks to anything else. Static analysis of runbook docs + traffic mirroring from switches on-prem. This is the artefact wave planning depends on.

3. A wave plan. Group applications into migration waves of 8–20 systems, ordered by:

  • Business criticality (low-risk waves first to prove the process)
  • Coupling (systems that talk to each other stay in the same wave)
  • Downstream dependency (databases before the apps that read them, unless replicating)

The wave plan is what the steering committee approves. Everything downstream refers back to it.

Phase 2: Landing Zone with Terraform, not Control Tower alone

Control Tower gets you a multi-account skeleton fast. It doesn't get you reproducible multi-account infrastructure.

The pattern that works:

terraform/ modules/ landing-zone/ # OUs, SCPs, log archive, audit account network-hub/ # Transit Gateway, DNS, direct connect account-baseline/ # IAM roles, Config, GuardDuty, VPC app-account/ # per-workload compute, storage, RDS environments/ prod/ stage/ dr/

Every account is instantiated by a Terraform workspace against a module. When you need account #47 for a migration wave, it's a terraform apply, not a ticket queue.

Phase 3: Terraform state strategy

Enterprise migrations fail in Terraform state as often as they fail in networking. Rules that keep it sane:

  • One state per account per environment. Never share state across accounts. Cross-account references go through remote state or SSM parameters.
  • S3 + DynamoDB locking, encrypted with a KMS CMK per organizational unit. Read-only access for engineers who shouldn't apply; apply-only via a CI role.
  • State is CI-only. No engineer runs terraform apply from their laptop against a shared workspace. Ever.
  • terraform_remote_state outputs are versioned. When the network module renames an output, downstream states break — treat module outputs as an API contract.

Phase 4: The MGN + Terraform interlock

For lift-and-shift workloads, MGN spins up the target EC2 instance. But you want that instance's VPC, subnet, security group, IAM instance profile, and CloudWatch agent config to come from Terraform.

The interlock:

  1. Terraform provisions the network posture (VPC, subnets, security groups, IAM roles) ahead of the wave.
  2. MGN launches the target instance into that pre-existing posture.
  3. Post-cutover, Terraform imports the launched instance so it becomes part of state and future updates flow through IaC.

The terraform import step is where teams get sloppy. Automate it — a small script that reads MGN's launch output and generates import blocks. Don't leave "MGN-launched instances" as a permanent exception in state.

Phase 5: The runbook, tested

Every wave gets a runbook, and every runbook is dry-run at least twice before cutover:

  • DR rehearsal — spin up the wave in a scratch account, run cutover procedures end-to-end.
  • Cutover rehearsal — same procedure, in the target account with production data mirrored (not migrated).
  • Actual cutover — third time you're doing this. It should feel routine.

The value of the rehearsals is not to find bugs in the Terraform. It's to time each step, identify who's on point for each decision, and build the muscle memory so 2am doesn't require thinking.

Phase 6: Post-migration — the FinOps hand-off

Migration cost estimates are always wrong on the low side once you're actually running production traffic. Budget for a 30-day tuning window per wave:

  • Rightsize compute (real usage vs. provisioned).
  • Move eligible workloads to Savings Plans / Reserved Instances.
  • Turn on S3 lifecycle policies for backups and logs.
  • Consolidate CloudWatch log groups (they accumulate cost quietly).

The Terraform modules make this rebalancing quick — an instance-type change is a variable update.

Anti-patterns to refuse

  • "We'll Terraform it later." You won't. It'll drift and the drift will bite during the first outage.
  • A single mega-state. Blast radius on every apply. Split by account and environment.
  • Console changes during migration. They will be undone by the next apply and blamed on Terraform.
  • Skipping the CAB. Even in a fast-moving migration, waves go through the customer's change advisory board. Skipping it is how you get a rollback mandate mid-wave.

Bottom line

Terraform doesn't make a migration faster. It makes the next migration wave faster, and the wave after that faster again — because the primitives are already reusable. The first wave with disciplined IaC costs slightly more than a lift-and-shift with console tweaks. Waves three through ten cost dramatically less.

Every wave I've delivered where Terraform was retrofitted after cutover eventually needed a "re-baseline sprint" that cost more than doing it right the first time. So do it right the first time.

LinkedIn