RizTech Academy logo
RizTech Academy
Terraform at ScaleLesson 2 of 540 min

Structuring a Terraform repository

A Terraform repository that works for one person and one environment becomes a tangle when it has to serve several environments and several engineers. How you structure the repository — where modules live, how environments are laid out, how state is split — determines whether the codebase stays maintainable or turns into copy-pasted chaos that drifts and breaks. This lesson is structuring a Terraform repository at scale, building on the Foundation's modules lesson and this module's state lesson.

The goals a good structure serves

Before a layout, be clear what you are optimising for — the structure exists to give you:

  • No duplication — the same infrastructure (a VPC, a service) defined once and reused, not copy-pasted per environment (the DRY principle; the Foundation's modules lesson).
  • Isolated state and blast radius — each environment (and often each component) has its own state (the state lesson), so a change or mistake affects only that slice, and prod cannot be touched while changing dev.
  • Environments that differ only where intended — staging and production share the same structure and differ only in inputs (sizes, counts), so they cannot drift in shape.
  • Reviewable changes — a change to one environment or component is a focused, understandable pull request.

Every structural choice below serves these goals. Get them and the repo scales to many environments and engineers; miss them and you get drift, duplication and dangerous coupling.

The core split: modules and environments

The foundational structure separates reusable modules from per-environment configuration:

terraform/
  modules/                    # reusable building blocks — NEVER applied directly
    vpc/                      # a reusable VPC (the networking module's design)
      main.tf  variables.tf  outputs.tf
    eks-cluster/
    service/
  environments/               # one directory per environment — each has its OWN state
    dev/
      main.tf                 # calls modules with dev inputs; its own backend/state
      backend.tf              # state key: dev/...
      terraform.tfvars        # dev-specific values
    staging/
    production/
  • modules/ holds the how — reusable definitions of your infrastructure pieces (a VPC module, an EKS-cluster module, a service module), written once, with input variables. These are never run directly; they are called.
  • environments/ holds the what for each environment — each environment directory is a thin configuration that calls the modules with environment-specific inputs, and has its own backend/state (the state lesson). An environment's main.tf is mostly module blocks:
module "vpc" {
  source     = "../../modules/vpc"
  cidr_block = "10.0.0.0/16"     # dev
  azs        = ["eu-west-1a", "eu-west-1b"]
}
module "cluster" {
  source       = "../../modules/eks-cluster"
  cluster_name = "dev"
  node_count   = 2               # dev: small; production would pass more
  vpc_id       = module.vpc.id
}

Staging and production have the same main.tf shape, passing bigger numbers. So the infrastructure is defined once in modules; the environments only supply inputs — which is exactly the "environments differ only in inputs" goal, and it means you cannot accidentally give production a different structure from staging, only different sizes.

Isolating state per environment (and component)

Combine this with the state lesson: each environment directory has its own remote state (its own key in the S3 backend). So:

  • Running terraform apply in environments/dev/ touches only dev's state and dev's resources — production is untouchable from there.
  • Production has its own state, changed only by running Terraform in environments/production/ (usually only via CI, with approval).

For larger systems you split further, per component: separate state for networking, the cluster, and the applications within an environment — so a change to the app does not lock or risk the networking. This is the blast-radius isolation the state lesson argued for, realised in the directory layout: state boundaries follow directory boundaries.

Keeping modules good

Because modules are the reusable heart of the repo, their quality matters (the code-quality lessons applied to infrastructure):

  • One module, one clear responsibility — a vpc module, an eks-cluster module — with documented input variables and outputs. A giant do-everything module is as bad as a giant function.
  • Pin module and provider versions — so an upstream change does not silently alter your infrastructure (the reproducibility discipline throughout this course). Reference registry modules by exact version too.
  • Use well-tested registry modules where they exist — for standard AWS infrastructure (a VPC, an EKS cluster), the community/official Terraform modules are battle-tested; calling one is usually wiser than hand-writing dozens of resources.
  • Compose small modules — a service module might call the VPC's outputs; environments wire modules together through their inputs and outputs.

Don't over-structure too early

A caution consistent with the whole course: introduce this structure when you have the need, not before. For a single environment and a few resources, a flat configuration is fine; the modules/environments split earns its complexity when you have a second environment (real duplication) or multiple engineers (real coupling risk). Building an elaborate module hierarchy for one small environment is premature abstraction — a cost with no payoff yet. Grow into the structure as environments and the team grow.

The picture to hold: modules/ for reusable pieces, environments/{dev,staging,production}/ each calling them with different inputs and owning its own state, split further per component as you grow, with well-scoped versioned modules. That structure gives no duplication, isolated blast radius, environments that differ only in inputs, and reviewable changes — which is what lets Terraform scale to a real AWS estate with many engineers.

Check your work

Goals: no duplication (define once, reuse), isolated state/blast radius (per-environment/component state), environments differing only in inputs (same shape, different sizes), reviewable focused changes.

Core split: modules/ = reusable how (vpc, eks-cluster, service — with input variables; never applied directly, only called); environments/{dev,staging,production}/ = thin what-for-here (call modules with env inputs; own backend/state). Env main.tf is mostly module blocks; all envs share the shape, differ in inputs.

State isolation: each environment directory has its own remote state (own key) → apply in dev/ touches only dev; production changed only from production/ (via CI + approval). Split further per component (networking/cluster/app) for finer blast radius — state boundaries follow directory boundaries.

Good modules: one responsibility + documented I/O; pin versions; prefer well-tested registry modules; compose small modules via inputs/outputs.

Don't over-structure early: flat config is fine for one small environment; adopt the split when you have a second environment or multiple engineers (real duplication/coupling). Premature abstraction is a cost.

Practice

  1. State the four goals a good Terraform repo structure serves, and why each matters at scale.
  2. Sketch the modules/environments directory layout and explain what lives where.
  3. Write an environment main.tf that calls a VPC and cluster module with environment-specific inputs.
  4. Explain how per-environment (and per-component) state isolates blast radius, tying to the directory layout.
  5. List three practices that keep modules maintainable and explain each.
  6. Explain when you would introduce this structure and why not before then.

Official documentation

Next: writing reusable modules.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship