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.
Directory-per-environment: the recommended approach
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 planand 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
applyruns only fromenvironments/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
applylocally 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
- Explain why "staging predicts production" requires the same shape and only intended differences.
- Sketch a directory-per-environment layout and show the difference between two environments' tfvars.
- Explain how to change infrastructure shape versus one environment's scale, and where each edit goes.
- Explain the environment-management trap with Terraform workspaces and why directories are safer for prod/staging/dev.
- Describe the safe promotion path for a module change from dev to production.
- Explain why production must be applied through a gated pipeline and never from a laptop.
Official documentation
- Terraform — Managing multiple environments — Composing configurations across environments.
- Terraform — Workspaces — What workspaces are and their limits.
- Terraform — Input variables (tfvars) — Supplying per-environment values.
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