Chapter 9: Organizing Larger Codebases
Topic 53

Environments — Workspaces vs Directories

EnvironmentsIsolation

Separating dev, staging, and prod is the most common organizational requirement, and Terraform offers two ways to do it: workspaces, where one codebase switches between several states, or separate directories, where each environment is its own root module. Chapter 3 introduced workspaces; this revisits the choice with the full organizational context and lands on why most teams isolate production with a directory and its own backend rather than a workspace.

The deciding factor is not how much code you duplicate — it is how much of a boundary stands between a careless command and production. A workspace puts that boundary one easily-forgotten command away; a separate directory and backend makes it a wall.

The Two Models

A workspace is a named, separate state under the same configuration and backend — terraform workspace select prod swaps the active state, and terraform.workspace lets the code branch small differences. The directory model gives each environment its own root module: environments/dev, environments/prod, each with its own backend block and tfvars, usually calling the same shared modules. Workspaces share everything but the state; directories share only the modules they choose to.

Two ways to split environments
Workspaces
One codebase and one backend; select switches the active state. DRY, but the boundary is one easily-forgotten command — easy to target the wrong env.
Directories
A root module per environment, each with its own backend and credentials. More repetition, but prod sits in an isolated state and account — a real wall.
Directory-per-environment: one root each, shared modules
environments/
├── dev/
│   ├── main.tf          # calls ../../modules/*
│   ├── backend.tf       # dev state bucket/key
│   └── terraform.tfvars # dev inputs
└── prod/
    ├── main.tf          # same module calls
    ├── backend.tf       # prod state, prod account
    └── terraform.tfvars # prod inputs

Isolation and Blast Radius

Separate directories give production its own state file, its own backend, and a natural place to use a different credentials profile or account. That is real blast-radius isolation: a mistake in the dev directory cannot reach the prod state, and prod can sit in a separate AWS account whose access is tightly held. Workspaces share one backend and one set of provider credentials across every environment — so the isolation is only as strong as remembering which workspace is active, and that is not strong enough for production.

DRY vs Explicit

Workspaces are the DRYer option: one copy of the code, several states. Directories are more explicit and more repetitive — but the explicitness is the point. With a directory you run apply in environments/prod and there is no hidden mode switching what that command targets. With a workspace, the same terraform apply hits dev or prod depending on state you set earlier and may have forgotten. Explicit beats clever wherever the cost of targeting the wrong environment is an outage.

The Hybrid and When to Use Each

The pattern most teams settle on keeps both virtues: thin per-environment root modules, each with its own backend and tfvars, all consuming the same shared modules. The shared modules carry the logic, so the only duplication is a few lines of backend config and a variable file — and that duplication is what buys the isolation. Workspaces still have a place: ephemeral, identical copies such as a per-pull-request or per-developer sandbox, where the environments are genuinely the same and short-lived. Reserve them for that and isolate long-lived, divergent environments — especially prod — with directories.

Workspaces vs directories for environments

Workspaces — one codebase, multiple states under a shared backend and credentials. Choose them for ephemeral, identical copies such as per-PR or per-developer stacks, where the boundary between environments does not need to protect anything.

Separate directories — a root module per environment with its own backend, tfvars, and an obvious apply target. Choose them for long-lived or divergent environments and always for prod, where a real isolation boundary and a different account are worth a few lines of repetition.

Common Mistakes
  • Using workspaces to separate prod and dev, then applying to prod because the active workspace was set to it earlier and forgotten.
  • Duplicating an entire environment config across directories instead of sharing a module and varying only the tfvars inputs.
  • Giving every environment the same backend and credentials, so there is no boundary actually protecting production from a dev mistake.
  • Diverging environments through sprawling terraform.workspace conditionals until each environment is a different code path nobody can read.
  • Assuming a workspace isolates the AWS account or credentials — it does not; every workspace shares the backend's and provider's auth.
Best Practices
  • Isolate production in its own directory with its own backend, credentials, and pipeline — ideally a separate AWS account.
  • Share a common module across environments and vary behavior through tfvars, staying DRY without losing isolation.
  • Reserve workspaces for ephemeral, identical copies such as per-PR and per-developer stacks.
  • Make the apply target explicit — a directory you cd into — so nobody applies to prod by accident.
  • Keep terraform.workspace branching minimal; heavy divergence is the signal to split into directories.
Comparable tools Pulumi stacks map naturally to per-environment isolation CloudFormation uses a separate stack per environment Terragrunt makes the directory-per-environment model DRY

Knowledge Check

What is the key isolation difference between workspaces and separate directories?

  • Directories give each environment its own backend and credentials boundary; workspaces share one backend and one set of credentials
  • Workspaces encrypt each environment's state with its own per-workspace key, while separate directories are forced to share a single encryption key across all of them
  • Directories can be applied in parallel while workspaces force every environment to apply strictly one at a time, because the shared backend serializes each run behind a single lock
  • There is no isolation difference; the choice is purely about code style and which layout the team happens to find easier to read

Why does production usually get a separate directory rather than a workspace?

  • A directory makes the apply target explicit and gives prod its own backend and account, so a careless command cannot silently hit prod
  • A workspace's state cannot hold more than a fixed number of resources, a hard cap that prod inevitably exceeds while a directory's state is allowed to grow without limit
  • Terraform flatly forbids combining workspaces with the S3 backend that production deployments require, so prod has to fall back to a plain directory layout instead
  • A directory plans noticeably faster, and prod plans have to finish within a strict time limit that a slower workspace-based plan would routinely blow past

How does the hybrid pattern keep things DRY without losing isolation?

  • Thin per-environment roots with their own backends all consume the same shared modules, so only backend config and tfvars are duplicated
  • Every environment shares one root module and a single state file, branching all of its per-environment logic on the value of the terraform.workspace name at apply time
  • A single shared tfvars file is symlinked into every environment directory to eliminate duplication entirely, so prod and dev always read precisely the same input values
  • Prod and dev share one backend and a single state file but pin different provider versions to stay separate, with the version pin standing in for a real isolation boundary

When are workspaces an appropriate choice?

  • For ephemeral, identical copies such as per-PR or per-developer sandboxes that are short-lived and need no protective boundary
  • For long-lived production environments that must be tightly isolated from every other stage, where a shared backend and one credentials set are exactly what you want
  • For environments that diverge heavily and each need a different pinned module version, since a single shared root can branch cleanly to cover all of those differences
  • Whenever you want each environment landed in its own separate AWS account, with the workspace name selecting which account's credentials get used for the run

You got correct