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).
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.
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.
modules/thing/ + envs/dev/ +
envs/prod/, where each env calls the module with different inputs
(e.g. instance_count = 1 vs 3).terraform init && terraform plan in each and confirm the states are independent.data "terraform_remote_state".