RizTech Academy logo
RizTech Academy
Terraform at ScaleLesson 4 of 535 min

Managing dev, staging and production

You have modules and a repo structure; now the practical question: how do you manage dev, staging and production so they stay consistent, differ only where you intend, and can be changed safely? Getting this wrong gives you environments that drift apart (so staging no longer predicts production) or a setup where changing dev risks production. This lesson is managing multiple environments with Terraform — the payoff of the structure the last lessons built.

The goal: identical shape, intended differences

The whole point of multiple environments is that staging predicts production — you test a change in staging and trust it will behave the same in production. That only holds if staging and production are the same shape, differing only in deliberate, known ways (sizes, counts, maybe a feature flag). So the goal is:

  • Same structure — both call the same modules, so they have the same kind of infrastructure.
  • Intended differences only — production has more/bigger (more replicas, larger instances, higher limits); the shape is identical, the scale differs. These differences are explicit inputs, not structural divergence.
  • No drift — nothing changes in one environment that does not go through the same code in the others.

Environment management is about achieving that: consistency by construction, with differences confined to inputs.

The approach this course recommends (from the repo-structure lesson) is a directory per environment, each calling the shared modules with its own inputs and its own state:

environments/
  dev/         main.tf  backend.tf  terraform.tfvars   # small: node_count=2, instance=small
  staging/     main.tf  backend.tf  terraform.tfvars   # medium
  production/  main.tf  backend.tf  terraform.tfvars   # large: node_count=6, instance=large

Each environment's main.tf calls the same modules; the differences live in terraform.tfvars:

# production/terraform.tfvars
environment  = "production"
node_count   = 6
instance_type = "m7g.large"    # Graviton (the compute module)
db_multi_az  = true            # production: highly available
# dev/terraform.tfvars
environment  = "dev"
node_count   = 2
instance_type = "t4g.small"
db_multi_az  = false           # dev: single-AZ, cheaper

Same modules, same structure, different values. To change the infrastructure shape, you change a module and it flows to every environment (after review); to change one environment's scale, you change its tfvars. And because each directory has its own state, running Terraform in dev/ cannot touch production. This is the clearest, most explicit way to manage environments — you can see each environment's full configuration in its directory.

A note on workspaces

Terraform has a feature called workspaces that lets one configuration hold multiple states (a "dev" workspace, a "prod" workspace) with the same code. It sounds like environment management, and for simple cases it can work — but it has a real trap for environments: because all workspaces share the same code, it is easy to apply to the wrong workspace (you switch workspace with a command, and forgetting which you are in can apply a change to production thinking it was dev). The directory-per-environment approach makes the target explicit — you are literally in the production/ directory — which is safer and clearer for the prod/staging/ dev case. Most teams prefer directories per environment for real environments, and reserve workspaces for transient or per-developer variations. For a foundation, prefer directory-per-environment; know workspaces exist but understand their environment-management pitfall.

Promoting changes safely

With this structure, changing infrastructure follows a safe promotion path (tying to the pipelines module):

  • Change a module or an environment via a pull request. CI runs terraform plan and posts it on the PR (the Foundation's plan-in-CI), so reviewers see exactly what will change — and you scrutinise every destroy/ replace (the Foundation's read-the-plan discipline), especially on production.
  • Apply to lower environments first. Change dev, verify, then staging, then production — the same progression as application deployments. A module change is tested in dev/staging before it reaches prod.
  • Gate production. Production apply runs only from environments/production/, through CI, with a manual approval (the pipelines module's environments) — so no one changes production without a reviewed plan and a deliberate approval.
  • Never apply production from a laptop. Production changes go through the reviewed, approved pipeline, not an engineer running apply locally against prod state — that is how accidents happen.

So multiple environments become: the same modules everywhere, per-environment inputs and state, changes flowing dev → staging → production through reviewed plans with production gated. That gives you environments that stay consistent (staging predicts production), differ only as intended, and cannot be changed dangerously — which is exactly what managing infrastructure at scale requires.

Check your work

Goal: staging predicts production, so both must be the same shape, differing only in intended inputs (sizes/counts/flags — scale differs, shape identical), with no drift (nothing changes in one env outside the shared code).

Directory-per-environment (recommended): environments/{dev,staging,production}/ each call the same modules with their own terraform.tfvars and own state. Change the shape → edit a module (flows to all); change one env's scale → edit its tfvars. Own state per directory → apply in dev/ can't touch prod. Explicit and clear.

Workspaces: one config, multiple states — trap for environments (shared code → easy to apply to the wrong workspace, e.g. prod thinking it's dev). Prefer directory-per-environment for real environments (target is explicit); reserve workspaces for transient/per-dev variations.

Safe promotion: change via PR with plan posted (read it — scrutinise destroy/replace, esp. prod); apply dev → staging → production; gate production (CI + manual approval); never apply prod from a laptop. Same modules everywhere, per-env inputs/state, reviewed plans, prod gated.

Practice

  1. Explain why "staging predicts production" requires the same shape and only intended differences.
  2. Sketch a directory-per-environment layout and show the difference between two environments' tfvars.
  3. Explain how to change infrastructure shape versus one environment's scale, and where each edit goes.
  4. Explain the environment-management trap with Terraform workspaces and why directories are safer for prod/staging/dev.
  5. Describe the safe promotion path for a module change from dev to production.
  6. Explain why production must be applied through a gated pipeline and never from a laptop.

Official documentation

Next: drift, imports and adopting existing resources.

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