Terraform #07 — Multi-Environment & Project Structure
Managing dev/staging/prod: workspaces vs directory-per-env, partial backend config, and when to reach for Terragrunt.
Goal: Manage multiple environments (dev/staging/prod) correctly. There are several valid approaches here, and the important thing is understanding the trade-offs — not just picking one blindly.
Common interview question: "How do you manage dev/staging/prod? Do you use workspaces? Why or why not?"
7.1. The core problem
You need the same infrastructure, but with:
- Separate state (a broken dev environment shouldn't affect prod).
- Different parameters (prod uses larger instances, more replicas).
- Different access controls (fewer people can apply to prod).
- Separate backends (often different accounts or buckets).
There are three main approaches. None is universally "correct" — it depends on context.
7.2. Approach 1: Workspaces
One codebase, multiple state files, distinguished by terraform.workspace.
terraform workspace new dev
terraform workspace new prod
terraform workspace select dev
terraform workspace listlocals {
instance_type = terraform.workspace == "prod" ? "t3.large" : "t3.micro"
replicas = terraform.workspace == "prod" ? 6 : 2
}
resource "aws_instance" "web" {
instance_type = local.instance_type
tags = { Environment = terraform.workspace }
}With an S3 backend, state is stored separately per workspace: env:/dev/..., env:/prod/....
Pros and cons of workspaces
| Pros | Cons |
|---|---|
| No code duplication | Easy to accidentally apply to the wrong workspace |
| Quick to switch | Shared backend config — hard to use separate accounts |
| Good for small, temporary variations | Hard to see environment differences through code/Git |
| Same provider version for all environments |
⚠️ The industry consensus: Workspaces are not ideal for real dev/staging/prod environments. HashiCorp themselves say this. Workspaces make sense for: temporary feature branch testing, or multi-region with identical config.
7.3. Approach 2: Directory per environment (recommended for production)
Each environment gets its own directory, but they all use the same shared modules.
infra/
├── modules/ # shared modules (single source of truth)
│ ├── network/
│ ├── database/
│ └── app/
└── environments/
├── dev/
│ ├── main.tf # calls modules with dev parameters
│ ├── backend.tf # dev-specific state location
│ └── terraform.tfvars
├── staging/
│ ├── main.tf
│ ├── backend.tf
│ └── terraform.tfvars
└── prod/
├── main.tf
├── backend.tf # separate state, potentially different account
└── terraform.tfvars
Pros and cons
| Pros | Cons |
|---|---|
| Clear separation, safe | Some code repetition (main.tf per env) |
| Each env can have its own backend/account | Requires discipline to keep envs in sync |
| Differences are visible in Git diffs | |
Hard to apply to the wrong env (have to cd) | |
| Each env can pin its own provider/module versions |
This is the most common and safest approach for real production environments. Reduce repetition through shared modules — accept a little duplication at the environment level.
7.4. Approach 3: Tooling (Terragrunt / Terramate)
When you have many environments, accounts, or regions, the code duplication becomes genuinely painful. Tools that generate DRY configurations:
- Terragrunt (Gruntwork): wraps Terraform, DRY backend + inputs, manages cross-stack dependencies. Very popular in larger organizations.
- Terramate: similar, code generation and orchestration.
- HCP Terraform / Terraform Stacks: HashiCorp's own multi-env/stack solution.
Example Terragrunt config (terragrunt.hcl) eliminating backend repetition:
# environments/prod/app/terragrunt.hcl
include "root" { path = find_in_parent_folders() }
terraform { source = "../../../modules/app" }
inputs = {
environment = "prod"
instance_type = "t3.large"
}Knowing Terragrunt is a meaningful plus. But get solid on plain Terraform first.
7.5. Partial backend config for multi-env (the key technique)
The backend block can't use variables. To use one codebase with multiple backends, use partial config (covered in Module 02):
# backend.tf
terraform {
backend "s3" {} # empty — details provided at init time
}# dev
terraform init -backend-config=backends/dev.hcl
terraform apply -var-file=vars/dev.tfvars
# prod
terraform init -reconfigure -backend-config=backends/prod.hcl
terraform apply -var-file=vars/prod.tfvarsbackends/dev.hcl:
bucket = "mycompany-tfstate-dev"
key = "app/terraform.tfstate"
region = "us-east-1"⚠️ When switching backends you need
terraform init -reconfigure(or-migrate-state). It's easy to forget — and applying against the wrong state is a very bad day. This is one reason directory-per-env is safer.
7.6. Quick comparison and recommendation
| Situation | Recommended approach |
|---|---|
| Learning / personal projects | Workspaces or directories |
| Serious dev/staging/prod | Directory per environment |
| Many accounts/regions/teams | Terragrunt / Terramate / Stacks |
| Temporary variations, multi-region identical config | Workspaces |
My recommendation: Start with directory-per-env and shared modules. When the code duplication becomes genuinely unwieldy (multiple accounts, many regions), look at Terragrunt.
7.7. Standard project structure (putting it all together)
repo/
├── modules/ # reusable modules
│ ├── network/
│ ├── compute/
│ └── data/
├── environments/
│ ├── dev/
│ ├── staging/
│ └── prod/
├── global/ # global resources (IAM, Route53, tfstate bucket)
├── .github/workflows/ # CI/CD (Module 08)
├── .tflint.hcl
├── .pre-commit-config.yaml
└── README.md
7.8. Hands-on Labs
In hands-on/ you'll find both approaches: a workspace demo and a directory-per-env demo (using the local provider, no cloud needed). Follow LAB.md:
- Lab 1 — Workspaces: create dev and prod, observe separate state, demo the "apply to wrong workspace" trap.
- Lab 2 — Directory-per-env: same module, two environments, two state files.
- Lab 3 — Compare both and figure out when you'd use each.
✅ Module 07 completion criteria
- Can explain the pros/cons of workspaces and why they're not great for production.
- Can set up a directory-per-environment structure with shared modules.
- Can use partial backend config for multiple environments.
- Know what problem Terragrunt solves.
- Can confidently answer the multi-environment interview question.
➡️ Next: Module 08 — Testing & CI/CD