How to organize Terraform modules for multiple environments?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
The goal of organizing Terraform for multiple environments is to reuse infrastructure logic without making every environment look identical by accident. A good structure keeps modules shared, state isolated, and environment-specific values explicit instead of hidden in copied directories.
Separate Reusable Modules From Environment Roots
A common and maintainable layout has two layers:
- Reusable modules in a
modules/directory - Thin environment root modules in an
envs/orlive/directory
Example:
The reusable modules define how resources are built. The environment roots decide which modules to call and with what values.
Keep Environment Roots Thin
Your dev, staging, and prod directories should mostly wire modules together rather than duplicate resource definitions.
Example envs/dev/main.tf:
Then prod can use the same modules but different values:
This keeps the environment differences visible without forcing you to maintain three copies of the same resource logic.
Isolate State Per Environment
Separate code is not enough. Each environment also needs isolated Terraform state so that a dev apply cannot accidentally modify prod.
For example:
Use a different state key for each environment, such as staging/terraform.tfstate and prod/terraform.tfstate. That boundary is as important as the directory structure itself. Without isolated state, a plan from one environment can read or overwrite resources that belong to another.
Use Variables for Intentional Differences
Modules should expose variables for things that truly vary:
- Instance counts
- SKU sizes
- CIDR blocks
- Feature toggles
- Tags and naming prefixes
What you want to avoid is hiding environment logic inside a module with hardcoded conditionals everywhere. Modules should stay reusable; environment roots should decide values.
What About Terraform Workspaces
Workspaces can help in some simpler setups, but they are usually not enough as the main organizational strategy for multi-environment infrastructure. Many teams prefer separate root directories because:
- The environment-specific configuration is easier to read
- Backend keys and provider settings are more explicit
- CI pipelines can target environments more safely
Workspaces are fine as a tool, but they should not replace clear environment boundaries when the infrastructure becomes serious.
Promote Module Versions Deliberately
As the codebase grows, environment separation also helps with promotion. For example, dev can point to a newer module version first, while prod stays on the previous version until validation is complete. That makes rollout safer than changing every environment at once.
Whether your modules come from local paths, a registry, or a Git source, treat module upgrades as an intentional environment change rather than an incidental side effect.
Common Pitfalls
- Copying full resource definitions into
dev,staging, andprodleads to drift and painful maintenance. - Putting too much environment-specific branching inside modules makes modules harder to reuse and test.
- Sharing one Terraform state file across environments is risky and defeats isolation.
- Treating workspaces as a complete environment-management strategy often becomes messy as the project grows.
Summary
- Keep reusable infrastructure logic in
modules/and environment entry points in separate root directories. - Reuse the same modules across environments, but pass different values intentionally.
- Isolate state per environment with separate backend keys or state locations.
- Prefer explicit environment roots over copy-pasted Terraform code.

