Skip to main content

Workspaces, Environments, and the Monorepo vs Polyrepo Question

What This Concept Is

Two overlapping layout questions that every Terraform team eventually argues about.

The environment question. You have dev, staging, and prod. How do you organize state and config so changes can progress from one to the next without risk of cross-environment damage?

Three common layouts:

  1. CLI workspaces -- one config, multiple named states (terraform workspace new prod). Isolated state per environment, shared code.
  2. Directory per environment -- envs/dev/, envs/staging/, envs/prod/, each with its own backend config and its own state. Shared modules in modules/.
  3. Repo per environment -- separate git repositories for each env. Hard isolation; hardest to keep in sync.

The repo question. Do you keep all infrastructure code in one monorepo (modules + envs + pipelines) or split it across many repos (one per team, one per service, or one per stack)?

These are not the same question, but they interact: workspaces work best inside a single repo; directory-per-env works in mono and poly equally; repo-per-env forces poly.

Why It Matters Here

Teams that get this wrong pay for it forever:

  • All environments share one state -> one mis-typed -target destroys prod instead of dev
  • Prod config drifted three versions behind dev because the two repos evolved independently
  • Workspaces used for "team A's infra" and "team B's infra" instead of environments -> credential confusion, cross-team state access

Decide early and write the choice down in an ADR (see Semester 7, Module 5).

Concrete Example

Directory-per-environment inside a monorepo (common production pattern):

infra/
modules/
vpc/
ecs-service/
rds-postgres/
envs/
dev/
main.tf # calls modules/vpc with dev inputs
backend.tf # S3 bucket=acme-tfstate-dev, key=infra/dev.tfstate
terraform.tfvars
staging/
main.tf
backend.tf
terraform.tfvars
prod/
main.tf
backend.tf
terraform.tfvars

Each envs/<env> has its own state, its own credentials scope, and its own apply cadence. Promotion from dev to prod is a PR that edits a module version pin in envs/prod/main.tf -- explicit and reviewable. The modules are tested once in dev before the pin is bumped in prod.

CLI workspaces (rare in production):

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

The config references the current workspace:

locals {
env = terraform.workspace
}

resource "aws_s3_bucket" "artifacts" {
bucket = "acme-artifacts-${local.env}"
}

This works and is terse. But it hides a dragon: if you forget to switch workspaces and apply, you just applied prod changes to dev (or worse, the reverse). HashiCorp's own documentation warns against using workspaces for decomposition into separate deployment credentials -- which is exactly what environments are.

Monorepo vs Polyrepo Tradeoffs

DimensionMonorepoPolyrepo
Module change reaches all callersOne PR, visibleBump version pins per repo
Access controlCoarse (repo-level)Fine-grained per repo
Blast radius of one PRCan touch everythingBounded by repo
Consistency / standardsEasy to enforceDrifts between repos
OnboardingOne cloneMany clones
Refactoring large surfacesPossiblePainful
CI cost / clarityOne pipeline, clever filteringMany pipelines, clear per-stack

There is no universal answer. Teams under ~30 infrastructure engineers often prefer monorepos; very large orgs or heavily regulated environments move to polyrepo for access control.

Common Confusion / Misconception

"Workspaces are the HashiCorp-recommended way to do environments." They are not. The official docs explicitly say workspaces "alone are not a suitable tool for system decomposition, because each subsystem should have its own separate configuration and backend." Use directory-per-env instead.

"Monorepo = slower CI." Only if you do not use path filtering. A well-configured pipeline only plans the environments that changed in a given PR.

"Polyrepo = better isolation." Better access isolation, yes. But polyrepo environments drift as teams fork modules and forget to merge back. Isolation through discipline (branch protections, required reviews) usually beats isolation through repo boundaries.

How To Use It

  1. Default to directory-per-environment with a monorepo of modules. This is the industry "boring production" answer.
  2. Put each environment's backend config in its own backend.tf -- never share a state key across environments.
  3. Use workspaces only for ephemeral state (PR previews, per-developer sandboxes), never for long-lived environments.
  4. If you inherit a workspace-per-env setup, migrate environment by environment; don't try a big-bang refactor.
  5. Document the layout in the repo's root README.md. New engineers should find the answer in 30 seconds.

Check Yourself

  1. Why is using terraform.workspace to distinguish prod from dev risky in practice?
  2. In a directory-per-env monorepo, how do you promote a module change from dev to prod?
  3. Give one specific scenario where polyrepo is clearly the right answer.

Mini Drill or Application

Sketch (on paper or in a markdown file) how you would lay out a small team's infrastructure:

  • one platform team of 4 engineers
  • three environments (dev, staging, prod)
  • two services (web, worker), each with its own VPC, ECS cluster, and RDS
  • shared modules

Produce the directory tree, state locations, and one paragraph explaining why you did not use workspaces.

See also (external)


Source Backbone

Infrastructure-as-code details are tool-specific, but these local books provide the operational backbone for shell, Git, and change discipline.