Skip to content
>_higan
This post has a translation:Read in Tiếng Việt

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 list
locals {
  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

ProsCons
No code duplicationEasy to accidentally apply to the wrong workspace
Quick to switchShared backend config — hard to use separate accounts
Good for small, temporary variationsHard 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.


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

ProsCons
Clear separation, safeSome code repetition (main.tf per env)
Each env can have its own backend/accountRequires 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.tfvars

backends/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

SituationRecommended approach
Learning / personal projectsWorkspaces or directories
Serious dev/staging/prodDirectory per environment
Many accounts/regions/teamsTerragrunt / Terramate / Stacks
Temporary variations, multi-region identical configWorkspaces

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:

  1. Lab 1 — Workspaces: create dev and prod, observe separate state, demo the "apply to wrong workspace" trap.
  2. Lab 2 — Directory-per-env: same module, two environments, two state files.
  3. 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

Share: