Reference · Terraform

Project Structure

State isolation drives the layout: environments, components, and how separate states talk.

The organizing principle

One state file = one blast radius, one lock, one pipeline. Project structure is not aesthetics — it is deciding how to partition state. Split along two axes: environment (dev / stage / prod) and component (network / data / app — grouped by rate of change and ownership).

The canonical layout (directory per environment)

infrastructure/
├── modules/                  # shared building blocks (or a separate versioned repo)
│   ├── vpc/
│   └── service/
└── envs/
    ├── dev/
    │   ├── network/          # one root module = one state file = one key
    │   │   ├── main.tf       #   backend key: dev/network/terraform.tfstate
    │   │   └── …
    │   └── app/
    ├── stage/
    │   └── …
    └── prod/
        ├── network/
        └── app/

Sources: HashiCorp: structuring for production · AWS Prescriptive Guidance · Gruntwork infrastructure-live

Workspaces vs directories

CLI workspacesDirectory per env
CodeOne copy, zero duplicationThin roots per env (some boilerplate)
StateSeparate state files, same backendFully separate state, backend, even account
CredentialsShared — dev access ≈ prod accessPer-env roles/accounts possible
Env differencesConditionals on terraform.workspace (grows ugly)Different inputs / module versions per env
Wrong-env riskOne forgotten workspace select awayYou're in the directory you're in
HashiCorp's own guidance: workspaces suit environments that barely deviate from each other; use directories when configuration differs, when different people manage different environments, or when prod needs its own credentials and access control. (Note: HCP Terraform "workspaces" are a different, heavier concept than CLI workspaces — don't conflate.)

Source: CLI workspaces docs

How separate states talk

# in envs/prod/app — read the network stack's outputs:
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "acme-terraform-state"
    key    = "prod/network/terraform.tfstate"
    region = "us-east-1"
  }
}

subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0]

Decision checklist