terraform · level 8

Workspaces & Environments

Two strategies for dev / staging / prod — and how to pick.

100 XP

Workspaces & Environments

Most projects need more than one copy of their infrastructure: dev, staging, prod. Two main strategies — pick one and stick with it.

Strategy 1: directory per environment

infra/
    modules/
        vpc/
        web/
    dev/
        main.tf            # uses modules with dev values
        backend.tf         # state at dev/terraform.tfstate
        terraform.tfvars
    staging/
        ...
    prod/
        ...

Each environment is a separate root module with its own backend (and therefore its own state file). To apply staging:

cd staging/
terraform init
terraform plan
terraform apply

The configs in dev/, staging/, prod/ look almost identical — they call the same modules with different inputs. Some duplication is the cost of clarity.

Pros: environments are visually distinct; mistakes in one can't accidentally hit another; per-env state, per-env locking, per-env backend. Cons: changes to "the system" require touching N directories. Tools like Terragrunt exist to deduplicate the boilerplate.

This is the dominant pattern in production codebases.

Strategy 2: workspaces

Terraform's built-in workspaces keep one config but multiple state files:

terraform workspace new dev
terraform workspace new prod
terraform workspace select dev
terraform apply

terraform workspace select prod
terraform apply

Inside the config you can branch on terraform.workspace:

locals {
  is_prod = terraform.workspace == "prod"
}

resource "aws_instance" "web" {
  count = local.is_prod ? 5 : 1
}

Pros: one config, many states. Quick for terraform init / branching dev environments per-feature. Cons: the workspace name is invisible if you're not careful — terraform apply in the wrong workspace happily destroys prod. Easy to mix up. Workspaces are best for ephemeral / per-developer envs, NOT for the dev/staging/prod hierarchy.

Backend per env

Whichever strategy, state must be separate per environment. A single state file holding both prod + dev means:

  • A terraform apply can accidentally touch prod when you meant dev.
  • Drift in dev shows up as plan noise on prod.
  • A corrupted state takes down both.

With workspaces, the backend automatically nests state per workspace. With directory-per-env, every directory configures its own backend key.

Variables per env

The most common pattern:

dev/terraform.tfvars      # dev defaults, gitignored if it has secrets
staging/terraform.tfvars
prod/terraform.tfvars

Or, with workspaces:

locals {
  config = {
    dev     = { instance_count = 1, instance_type = "t3.micro" }
    staging = { instance_count = 2, instance_type = "t3.small" }
    prod    = { instance_count = 5, instance_type = "m5.large" }
  }

  cfg = local.config[terraform.workspace]
}

resource "aws_instance" "web" {
  count         = local.cfg.instance_count
  instance_type = local.cfg.instance_type
}

A single map keyed by env name. New environment? Add a key.

Terragrunt: the popular layer on top

Terragrunt is a thin wrapper around Terraform that deduplicates the directory-per-env pattern. You declare each environment in a terragrunt.hcl that points at a shared module + supplies the env-specific inputs. Backends, providers, and dependencies get auto-generated.

If you find yourself maintaining identical main.tf files across dev/, staging/, prod/, look at Terragrunt before writing a custom code-gen.

Don't share a workspace across teams

Workspaces are an organisational primitive WITHIN your config — they're not access control. Anyone with the credentials to apply your config can switch to any workspace. For real per-team / per-env isolation, use separate AWS accounts (or GCP projects, Azure subscriptions) and separate state backends. Terraform workspaces aren't a security boundary.

Tip

Whichever strategy you pick, name and label everything with the environment. Tagging every resource with Environment = var.environment (or terraform.workspace) means a misapplied stack is recognisable in the cloud console at a glance.