BXUP Track progress & test yourself — free
DevOps / SRE / Platform · Playbook · Advanced · ~4 min read
DEVOPS / SRE / PLATFORM · PLAYBOOK · ADVANCED

Refactor Legacy IaC into Modules with State-Move Planning

Every mature Terraform estate has the 4,000-line main.tf nobody wants to touch. Refactoring it into modules is high-value and high-dread, because the code change is the easy half — the hard half is moving state addresses so Terraform does not interpret your reorganization as "destroy everything and recreate it."

Tier: Advanced · Workflow area: Infrastructure as Code

Large-context AI assistants are genuinely strong at the code restructuring and at drafting the state-move plan; the discipline is that state surgery is generate-only, human-executed, and gated by a zero-diff plan.

When to use

  • Monolithic root modules that multiple teams edit and collide in.
  • Copy-pasted stacks across environments that should be one parameterized module.
  • Pre-work before adopting workspaces, stacks, or a new repo layout.

Skip this on infrastructure scheduled for decommission, and never combine a refactor with functional changes — one intent per PR.

Prerequisites

  • A repo-aware AI assistant with large context (it must see the whole root module).
  • Terraform ≥ 1.1 (moved blocks — strongly preferred over CLI state mv).
  • State backup discipline and a change freeze for the affected stack during cutover.
  • The full verification chain from pb-08 wired in CI.

The workflow

  1. Map before you move. Have the assistant build the inventory first — it forces both of you to understand the blast radius:

    Read main.tf, networking.tf, data.tf in this repo. Produce:
    1. A table of every resource: address, type, and which logical group it
       belongs to (network / cluster / database / iam / monitoring).
    2. The dependency edges between groups (which references cross groups).
    3. A proposed module decomposition that minimizes cross-module references,
       with which variables/outputs each module boundary needs.
    Do not write any code yet.
    

    Review the decomposition like an architecture doc — module boundaries outlive this refactor.

  2. Generate the refactor with addresses preserved via moved blocks.

    Implement the approved decomposition. Rules:
    - Resource CONFIGURATION must be byte-for-byte equivalent — this refactor
      changes structure only, no attribute changes, no version bumps.
    - For every resource that changes address, emit a moved block:
        moved {
          from = aws_subnet.private_a
          to   = module.network.aws_subnet.private["a"]
        }
    - Where count is replaced by for_each, enumerate every index-to-key move
      explicitly.
    - Emit the moved blocks in a single moves.tf so they are reviewable
      together and removable after cutover.
    
  3. Gate: the plan must show moves only. This is the entire safety argument:

    terraform plan -out=refactor.plan
    terraform show refactor.plan | grep -E "will be (destroyed|created|replaced)" && echo "STOP: not a pure refactor"
    

    The acceptable plan says moved / Plan: 0 to add, 0 to change, 0 to destroy. Any create/destroy pair means a missed or wrong moved block — fix it before proceeding. Do not rationalize "it's just recreating a harmless resource"; harmless resources are how the habit of accepting destroys forms.

  4. Cross-check the moves list independently. Diff the assistant's moves against reality: terraform state list before vs the planned addresses after. Every old address must map somewhere; count mismatches mean orphans that will be destroyed. This is the citation-check step — do not skip it because the plan "looked clean."

  5. Apply during a freeze; verify zero-diff; clean up. Apply (human hands, state backup taken), then immediately terraform plan again expecting No changes. After a soak period, remove moves.tf in a follow-up PR.

  6. For fleets: refactor one environment fully, extract the pattern, then have the assistant replay the same decomposition per environment — each with its own moves-only plan gate. Never batch-apply across environments on one review.

Warning

If the assistant proposes terraform state rm plus re-import as a shortcut, or offers to run state commands itself — refuse. moved blocks are declarative, reviewable, and roll back with git; imperative state surgery is neither, and an agent must never execute it.

Tool variants

  • Claude Code / large-context repo agents: the primary tool — this task is exactly what long context is for. Let it run plan (read-only); never apply.
  • Pulumi users: aliases (aliases: [{name: oldName}]) play the moved role; agentic tools like Pulumi Neo can drive refactors — hold them to the same preview-shows-no-replaces gate.
  • Chat assistant without repo access: workable for smaller roots — paste files and terraform state list output; expect more iteration on the moves list.

Verification

  • Validate: refactored code passes terraform validate + TFLint; module boundaries expose only the variables/outputs the dependency map requires.
  • Plan: the moves-only gate — zero add/change/destroy, moves matching the independently-checked list; post-apply plan shows No changes.
  • Policy: Checkov/Conftest output identical before vs after (a pure refactor changes no policy posture — any delta is a smuggled functional change).
  • Cost: Infracost diff is exactly zero — same argument as policy.
  • State safety: backup taken pre-apply; freeze honored; moves.tf cleanup PR merged after soak.

Pitfalls

  • count→for_each address explosions.aws_instance.app[0] → aws_instance.app["blue"] needs an explicit move per index; assistants get the pattern right and individual indices wrong. The independent cross-check catches this.
  • Accidental "improvements." Version bumps, tag additions, renamed variables — assistants cannot resist. The byte-for-byte rule plus the policy/cost-delta-zero checks exist to catch it.
  • Providers inside modules. Moving resources into modules that declare their own providers changes behavior (aliasing, default tags). Keep provider config at root unless you know why not.
  • Refactoring live-changing stacks. Someone else's apply mid-refactor invalidates your plan. Freeze or fail.

Restricted-environment variant

The full root module and state listing describe the client's entire architecture. This playbook should run inside the client boundary by default: in-tenancy AI endpoint (Bedrock/Azure OpenAI) for the mapping and generation steps, OSS chain for everything else. Where no in-tenancy model exists, the decomposition design (step 1) can be done on a structurally-redacted resource table (types + relationships, names hashed), with code generation done by hand or with a locally-hosted model.

Success criteria

  • Refactor merged with a moves-only plan, independently verified moves list, and post-apply No changes.
  • Policy and cost outputs provably identical before/after.
  • No state command executed by an agent; state backup retained.
  • The next change to this stack touches one small module instead of the monolith — the actual point.
This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in