Terraform Workspaces vs Directories: Choosing an Environment Strategy
The Question Behind the Question
Teams rarely struggle to create a dev workspace. They struggle to decide how much isolation production requires. The environment pattern controls state blast radius, credentials, approvals, provider versions, and whether a developer can accidentally plan against production.
Terraform CLI workspaces provide multiple state instances for one configuration and backend. They are useful, but they are not a general security boundary. HCP Terraform workspaces are a different product concept with their own execution, variables, permissions, and state.
---
Pattern 1: CLI Workspaces
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
terraform workspace select dev
terraform workspace show
Configuration can derive settings from the current workspace:
locals {
environments = {
dev = { instance_size = "small", replica_count = 1 }
staging = { instance_size = "medium", replica_count = 2 }
prod = { instance_size = "large", replica_count = 4 }
}
env = local.environments[terraform.workspace]
}
This is compact and reduces duplication. It works well for short-lived preview stacks, training sandboxes, and multiple copies of the same application under the same trust boundary.
The risks are operational. The selected workspace is local CLI context, so a command run from the right directory can still target the wrong state. All environments share the same backend configuration and usually the same provider wiring. Conditional expressions accumulate as environments diverge. A production plan may be only one workspace select away from a development session.
Do not use CLI workspaces for environments requiring separate credentials, access controls, or administrative boundaries. Practice their mechanics in the workspace environments lab, but distinguish convenience from isolation.
---
Pattern 2: Directory Per Environment
infrastructure/
|-- modules/
| |-- service/
| `-- database/
|-- environments/
|-- dev/
| |-- backend.tf
| |-- main.tf
| `-- terraform.tfvars
|-- staging/
`-- prod/
Each environment is a root module calling shared child modules:
module "checkout" {
source = "../../modules/service"
environment = "prod"
instance_size = "large"
replica_count = 4
deletion_guard = true
}
Each directory can have a different backend key, credential role, provider constraints, and CI approval policy. A production job can be restricted to environments/prod, while pull requests still validate all roots.
The cost is repetition. Provider and module call changes may need updates in several directories. Reduce that cost with small root modules, shared child modules, automated formatting and validation, and dependency tooling if the estate becomes large. Do not hide every difference behind a giant wrapper; visible root configuration is often valuable review context.
---
Pattern 3: Separate Repositories or Stacks
Some boundaries deserve complete separation: regulated production, different business units, or infrastructure with independent release cadences. Separate repositories can provide distinct ownership, secrets, branch protection, and audit trails.
The tradeoff is coordination. Shared module releases must be versioned. Cross-stack dependencies need narrow, stable interfaces, such as published DNS names or selected remote-state outputs. A monorepo can still contain separate roots and pipelines; repository count is not the only isolation mechanism.
---
Compare the Patterns
| Concern | CLI workspaces | Environment directories | Separate roots/repos |
|---|---|---|---|
| State isolation | Separate state names | Separate backend configs | Fully separate |
| Credential isolation | Awkward | Straightforward | Strongest |
| Configuration reuse | Highest | Shared modules | Versioned modules |
| Environment differences | Conditionals/maps | Explicit per root | Explicit per stack |
| Wrong-target risk | Higher | Lower | Lowest with policy |
| Best fit | Ephemeral copies | Most dev/stage/prod teams | Hard boundaries |
---
A Safe CI/CD Layout
For directory-based environments, derive the target from changed paths, never from an untrusted free-form workspace string. A production pipeline should:
- Authenticate with a short-lived production role.
- Run
terraform init -reconfigurein the production root. - Run
fmt -check,validate, and policy/security checks. - Create
terraform plan -out=tfplan. - Publish the human plan and machine-readable plan for review.
- Require approval, then apply that exact saved plan.
- Serialize applies for that state and retain logs.
Never generate one plan and then apply after silently changing variables, provider credentials, or backend context. Saved plans capture important context and may contain sensitive values, so protect them like state.
Recommendation
For most long-lived dev, staging, and production environments outside HCP Terraform, use small directory-based roots that call shared modules. Use CLI workspaces when the copies are structurally identical and share a trust boundary. Split roots further when state becomes too large or ownership and release cadence differ.
The Terraform theory course builds the mental model; the cross-state outputs lab lets you practice designing a narrow interface between isolated roots.