When One Infrastructure Config Has to Serve Multiple Environments
You have a Terraform configuration that defines your VPC, subnets, and load balancer. It works perfectly for your development environment. Now you need the same setup for staging and production. The naive approach is to copy the entire folder three times and change a few variable values. That works until you need to update a security group rule, and you realize you have to make the same change in three places, hoping you don't miss one.
The problem is not about writing infrastructure code. The problem is about managing the same code across multiple environments without duplicating everything or accidentally breaking production while working on development.
Two common approaches solve this: workspaces and separate root modules. They handle the same problem in fundamentally different ways, and choosing between them depends on how your team operates and how much separation your environments need.
Workspaces: One Codebase, Multiple States
Workspaces are a feature built into tools like Terraform that let you use the same source code with different state files. Think of it as having one folder of configuration that can produce three different environments. The code is identical. What changes is the state file that tracks what has been deployed.
The workflow is straightforward. You create a workspace for each environment, then switch to the correct workspace before running any changes. The tool automatically routes state reads and writes to the correct backend location based on which workspace is active. If you are in the development workspace, your changes affect only the development state. Production state stays untouched.
The diagram below illustrates the structural difference between the two approaches.
Here is how that workflow looks in practice:
# Create a workspace for each environment
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
# Switch to the development workspace
export TF_WORKSPACE=dev
# Or use: terraform workspace select dev
# Plan and apply changes for the active workspace
export TF_VAR_instance_type=t3.micro
terraform plan -out=dev.tfplan
terraform apply dev.tfplan
# Switch to production when ready
export TF_WORKSPACE=prod
export TF_VAR_instance_type=t3.large
terraform plan -out=prod.tfplan
terraform apply prod.tfplan
This works well when your environments are similar and the differences are limited to variable values. Maybe development uses smaller instance sizes, staging uses medium instances, and production uses the largest. The structure is the same. Only the numbers change.
But workspaces have a hidden cost. Because all environments live in the same codebase, the risk of operating on the wrong workspace is real. A developer working in the development workspace can accidentally run a plan or apply against production if they forget to switch. The tool does not prevent this. It only tracks which workspace is active. Human attention becomes the safety barrier.
There is another limitation. When you change the configuration structure, every environment gets the change at the same time. You cannot upgrade staging first, verify it works, and then roll the same change to production a day later. All environments move together because they share the same code. If the change breaks something, it breaks everywhere simultaneously.
Root Modules: Separate Folders, Shared Logic
The alternative is to give each environment its own root module. Instead of one folder with three workspaces, you have three folders: dev, staging, and prod. Each folder contains its own main configuration file and its own state backend configuration. They are completely independent.
But you do not rewrite the infrastructure logic three times. Each root module calls the same shared modules from a modules directory. The VPC definition, the subnet logic, the load balancer configuration all live in reusable modules. The root modules just call those modules with environment-specific variables.
The advantage is clear. You cannot accidentally modify production while working in development because you have to explicitly change directories. The separation is physical, not just logical. You can also update environments independently. Production can stay on a stable version of a module while development uses the latest version with experimental changes. Staging can be updated first, tested, and only then promoted to production.
The downside is duplication, but it is structural duplication, not logical duplication. You repeat the module calls and the variable definitions for each environment. You do not repeat the actual infrastructure logic. The modules contain the logic, and they are shared. The duplication is in the wiring, not in the implementation.
When to Use Which
Workspaces fit small teams with few environments and minimal differences between them. If you have two environments that are almost identical, workspaces reduce overhead. You write the code once, and the workspace handles the state separation.
Root modules work better for larger teams, environments that need strict separation, or situations where each environment requires different approval workflows. If production changes need a pull request review, a manager approval, and a change window, while development changes can be applied directly, root modules make that separation natural. Each environment folder can have its own CI/CD pipeline with different gates.
Many teams start with workspaces because they are simpler to set up. As the team grows and the number of environments increases, they migrate to root modules. The migration is not painful because the modules are already separated. You just create new root folders that call the same modules.
A Practical Checklist
Before choosing your approach, run through these questions:
- How many environments do you manage? More than three pushes toward root modules.
- Do environments need different approval processes? Yes means root modules.
- Can a mistake in one environment affect another? If yes, root modules reduce that risk.
- Is your team size more than five people? Larger teams benefit from explicit folder separation.
- Do you need to upgrade environments at different times? Root modules make phased rollouts possible.
If you answered yes to three or more of these, start with root modules. If you answered no to most of them, workspaces will serve you well for now.
The Concrete Takeaway
Workspaces and root modules solve the same problem with different trade-offs. Workspaces reduce code duplication but increase operational risk. Root modules add structural duplication but give you clear boundaries between environments. Choose based on how your team works and how much isolation your environments need. Start simple, but know when to switch. The goal is not to pick the perfect approach on day one. The goal is to recognize when your current approach starts causing more problems than it solves.