Terraform Workspaces vs Directories: When Each One Makes Sense
Workspaces look like the obvious way to manage dev, staging, and prod — until they aren't. Here's how to choose between workspaces and directory-per-environment without painting yourself into a corner.
- #terraform
- #workspaces
- #environments
- #state
- #iac
- #ai
Every Terraform codebase eventually hits the same fork in the road: you have a dev, a staging, and a prod, and you need a way to keep their state separate without copy-pasting your entire module tree three times. Terraform hands you terraform workspace and the docs make it look like the answer. It usually isn’t — at least not the way most teams reach for it.
I’ve migrated more than one team off workspaces after they discovered, mid-incident, that a single misconfigured select had pointed a prod apply at the staging state. Here’s how I decide between workspaces and directory-per-environment, and how to keep either one from biting you.
What workspaces actually are
A workspace is just a named state file inside the same backend. When you run terraform workspace new staging, you don’t get a new directory or a new config — you get a second state file, and the code you run against it is identical. The only thing that changes is terraform.workspace, a string you can branch on.
locals {
instance_count = terraform.workspace == "prod" ? 6 : 1
instance_type = terraform.workspace == "prod" ? "m5.2xlarge" : "t3.medium"
}
resource "aws_instance" "app" {
count = local.instance_count
instance_type = local.instance_type
ami = var.ami_id
}
That’s the whole model: one set of code, N state files, environment differences expressed as conditionals on a magic string.
Why that magic string is dangerous for environments
The problem is that nothing about your shell tells you which workspace you’re in. The directory looks the same. The files look the same. The only signal is terraform workspace show, which nobody runs before every apply. So the failure mode is brutally simple: you think you’re in staging, you’re actually in prod, and your plan applies before you read it carefully.
Workspaces also tempt you into ternary sprawl. Once you have three of them, every meaningful difference becomes a terraform.workspace == "..." conditional, and your config slowly turns into a nest of branches that no one can reason about. The blast radius of a typo in one of those conditionals is an entire environment.
And because all workspaces share one backend configuration, they often share one set of credentials and one state bucket. That’s the opposite of the isolation you want between prod and everything else.
The directory-per-environment alternative
The pattern I reach for instead is boringly explicit: a directory per environment, each with its own backend block, each calling a shared module.
environments/
dev/
main.tf
backend.tf
terraform.tfvars
staging/
main.tf
backend.tf
terraform.tfvars
prod/
main.tf
backend.tf
terraform.tfvars
modules/
app/
main.tf
variables.tf
outputs.tf
Each environment’s main.tf is a thin wrapper:
module "app" {
source = "../../modules/app"
environment = "prod"
instance_count = 6
instance_type = "m5.2xlarge"
}
And each has its own backend, with a distinct state key and ideally a distinct bucket or account:
terraform {
backend "s3" {
bucket = "acme-tfstate-prod"
key = "app/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "tf-locks-prod"
}
}
Now your location is your environment. cd environments/prod is an unmissable signal. The diffs between environments live in terraform.tfvars, not in conditionals scattered through the code. And you can scope IAM so the prod state bucket is only reachable with prod credentials — real isolation, not a shared bucket with three keys.
So when are workspaces actually good?
Workspaces shine when the environments are genuinely identical and ephemeral. Think:
- Per-developer sandboxes that are throwaway copies of dev.
- Per-pull-request preview environments spun up and torn down by CI.
- Per-region replicas of the exact same stack, where the only difference is a provider region you pass in.
In those cases the “identical code, separate state” model is exactly right, and the conditionals stay shallow because the environments really are the same shape. A CI job that runs terraform workspace new pr-1234, applies, and later destroys it is a clean use of the feature.
The rule I use: workspaces for fungible, short-lived copies of one environment; directories for the long-lived environments you actually care about. The moment an environment needs different IAM, different sizing baked into code, or a different blast radius, it deserves its own directory and backend.
A migration path off workspaces
If you’re already on workspaces and feeling the pain, you don’t have to do it all at once. Stand up the new directory structure, then for each environment pull its state and re-point it:
# From the old workspace-based config
terraform workspace select prod
terraform state pull > prod.tfstate
# In the new environments/prod directory, after terraform init
terraform state push prod.tfstate
Run a terraform plan afterward and confirm it shows no changes — that’s your proof the state moved cleanly. Do it one environment at a time, prod last, and keep the old config until every plan comes back empty.
When you’re reviewing those migration diffs, an extra set of eyes helps catch a backend key that’s subtly wrong before it creates a duplicate resource. That’s the kind of review our AI code review tooling is built to speed up.
The short version
Workspaces are a state-isolation primitive, not an environment-management strategy. Use directory-per-environment for the environments you’d be paged about, and save workspaces for the cheap, identical, disposable copies where their convenience actually pays off.
For more patterns on structuring real-world Terraform, see the rest of our Terraform guides.
Examples are illustrative. Always validate state migrations against a plan that shows zero changes before trusting them in production.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.