Terraform
Infrastructure as Code
DevOps
Environment Management
Module Organization

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/ or live/ directory

Example:

text
1terraform/
2  modules/
3    network/
4    app_service/
5    postgres/
6  envs/
7    dev/
8      main.tf
9      variables.tf
10      terraform.tfvars
11    staging/
12      main.tf
13      variables.tf
14      terraform.tfvars
15    prod/
16      main.tf
17      variables.tf
18      terraform.tfvars

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:

hcl
1module "network" {
2  source              = "../../modules/network"
3  environment         = "dev"
4  address_space_cidr  = "10.10.0.0/16"
5}
6
7module "app_service" {
8  source              = "../../modules/app_service"
9  environment         = "dev"
10  instance_count      = 1
11  subnet_id           = module.network.app_subnet_id
12}

Then prod can use the same modules but different values:

hcl
1module "app_service" {
2  source              = "../../modules/app_service"
3  environment         = "prod"
4  instance_count      = 3
5  subnet_id           = module.network.app_subnet_id
6}

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:

hcl
1terraform {
2  backend "azurerm" {
3    resource_group_name  = "tfstate-rg"
4    storage_account_name = "mytfstateacct"
5    container_name       = "tfstate"
6    key                  = "dev/terraform.tfstate"
7  }
8}

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, and prod leads 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.

Course illustration
Course illustration

All Rights Reserved.