Lesson 0005 · Understand Terraform

Structuring real projects

One idea · ~12 min. Project layout is state partitioning — everything else is decoration.
Why this next? This completes your "how do I structure real projects" black box. You now have all the prerequisites: state (lessons 1–2), one-lock-per-state-file (lesson 3), and modules as the unit of reuse (lesson 4). Structure is just composing those.

The question "how should I lay out my Terraform repo?" is really the question "how should I partition state?" One state file = one blast radius, one lock, one pipeline. Cut along two axes: environment (dev/stage/prod) and component (network/data/app — split by ownership and rate of change).

The canonical shape

infrastructure/
├── modules/            # the logic — shared, versioned building blocks
│   ├── vpc/
│   └── service/
└── envs/
    ├── dev/app/        # thin roots: backend + provider + module calls + inputs
    ├── stage/app/      # one directory = one state file = one blast radius
    └── prod/app/       #   backend key: prod/app/terraform.tfstate

Each env directory is a thin root module — backend block, provider config, module calls, env-specific values. Logic never lives in env dirs. Environments differ in inputs (and module versions — prod on v2.3 while dev trials v3.0), not in implementation. The moment you copy-paste a resource block between env dirs, the envs start drifting apart silently.

The workspaces question. CLI workspaces give you separate state files on the same backend with the same credentials, switched by a mood-dependent terraform workspace select. HashiCorp's own guidance: fine for environments that barely deviate; use directories when resources, people, or credentials differ. Prod deserves its own access boundary — a workspace can't give it one.

Separate states still need to talk. data "terraform_remote_state" reads another state's root-level outputs (encapsulation again — internals stay private), takes no lock, and creates tight coupling. The looser alternative: plain data sources — look the VPC up by tag, survive the other stack's refactors. And note the security angle: reading a state file means reading all of it, secrets included — across team boundaries, prefer data sources or explicitly shared parameters.

You're the platform engineer — you call it

✅ Prove it on your own machine (optional, 15 min)

  1. Restructure any config into modules/thing/ + envs/dev/ + envs/prod/, where each env calls the module with different inputs (e.g. instance_count = 1 vs 3).
  2. Give each env its own backend key (or local state for the exercise) — run terraform init && terraform plan in each and confirm the states are independent.
  3. Add an output to dev, then read it from another root with data "terraform_remote_state".
💬 Good follow-ups: "What does Terragrunt add on top of this layout?" · "How do teams handle per-env AWS accounts and provider assume_role?" · "When is one repo vs many repos right?"