Originally published on kuryzhev.cloud
One shared Terraform backend and a forgotten workspace select is all it takes to apply your staging changes straight to prod. I've seen it happen on a team that was otherwise disciplined — good PR reviews, good CI, decent test coverage. The terraform workspaces vs backend isolation question came up in their retro, and it should have come up months earlier.
When you face this choice
This decision usually sneaks up on you. You start with one AWS account, a handful of resources, and folders like dev, staging, prod. It works fine for a while. Then the team grows, someone spins up a second AWS account for production isolation (as they should), and the question lands in a Slack thread: "should we just use terraform workspace for this?"
It's a reasonable question. Workspaces are built into core Terraform, they need zero extra setup, and the docs make them sound like exactly the multi-environment primitive you want. But the real thing you're managing here isn't file organization — it's blast radius. What happens when one bad apply lands in the wrong place?
The two philosophies are genuinely different, not just cosmetic variants of each other:
- Workspaces — same backend, same bucket, same credentials, different state key inside it.
- Separate backends — different bucket, often different AWS account, different IAM role, full stop.
Get this wrong early and you inherit a migration project later. Get it right and you never think about it again.
Option A — Native Terraform workspaces (pros/cons)
Mechanically, workspaces are simple. Running terraform workspace new staging doesn't provision anything new — it just creates a state entry at env:/staging/<key> inside the exact same backend every other workspace uses. Same S3 bucket. Same DynamoDB lock table. Same IAM permissions attached to whoever's running Terraform.
Pros:
- Zero extra infrastructure to bootstrap — no bucket-per-env chicken-and-egg problem.
- Fast to spin up ephemeral environments; great for short-lived stacks.
- One
terraform.workspaceinterpolation drives naming and tagging across all envs.
Cons:
- Shared backend means shared blast radius — a corrupted state file or a bad lock affects every environment sharing that bucket.
- Shared IAM role across environments violates least privilege. A leaked CI token with workspace access can touch every environment's state, prod included.
- It's easy to forget which workspace is currently selected.
terraform workspace showbefore every apply is not optional discipline — it's the only thing standing between you and applying the wrong plan.
Here's the failure mode I've actually watched happen: an engineer runs terraform apply believing they're in staging. They're actually in default, left over from a previous session. Terraform doesn't warn you — it just applies. Watch out: a workspace-driven AWS provider block doesn't switch accounts for you either. Provider config is not workspace-aware, so if you're assuming workspaces will point you at a different account/region automatically, you're wrong, and that assumption alone has caused more than one accidental cross-account apply.
Option B — Directory-per-environment with isolated backends (pros/cons)
This is the pattern most teams converge on once they've been burned once. Structure looks like envs/dev/, envs/staging/, envs/prod/, each with its own backend.tf pointing at a distinct S3 bucket and, ideally, a distinct AWS account entirely.
# envs/prod/backend.tf — isolated backend, own account, own lock table
terraform {
backend "s3" {
bucket = "acme-tfstate-prod" # dedicated bucket, prod account only
key = "network/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "acme-tf-locks-prod" # separate lock table = separate blast radius
encrypt = true
}
}
# envs/staging/backend.tf — same structure, different account/bucket
terraform {
backend "s3" {
bucket = "acme-tfstate-staging"
key = "network/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "acme-tf-locks-staging"
encrypt = true
}
}
# Contrast: the workspace approach shares ONE backend block for all envs
terraform {
backend "s3" {
bucket = "acme-tfstate-shared" # every env's state lives here
key = "network/terraform.tfstate" # workspace name gets prefixed automatically
region = "us-east-1"
dynamodb_table = "acme-tf-locks-shared"
encrypt = true
}
}
Pros:
- True blast-radius containment — a prod incident can't touch staging state because it literally can't authenticate to that bucket.
- Per-environment IAM roles enforce least privilege natively, not by convention.
- Each state file stays small, so
planandapplystay fast even as the org grows.
Cons:
- Module-invocation code gets duplicated across env directories (shared modules mitigate this, but don't eliminate the boilerplate).
- Bootstrap chicken-and-egg problem — the backend infra itself needs state somewhere before Terraform can manage anything. Usually solved with a one-time manual apply or a dedicated bootstrap module.
- More moving parts for a two-person team managing three environments. This overhead is real, not imaginary.
Terragrunt, or plain -backend-config= flag files, is the standard tooling fix for the copy-paste problem:
terraform init -backend-config=backend-prod.hcl
Decision matrix
Skip the prose and check the table. This is what I actually walk teams through when they're stuck on terraform workspaces vs backend isolation.
Decision matrix — pick your isolation strategy
Criteria | Workspaces | Separate backends
-----------------------------------|-------------------|-------------------
Single AWS account, dev only | Fine | Overkill
Prod in its own account | Wrong tool | Required
Compliance / audit requirements | Insufficient | Required
Short-lived PR preview envs | Good fit | Too heavy
Small team, low risk tolerance ok | Acceptable | Safer default
IAM least-privilege enforcement | Hard | Native
State/plan performance at scale | Degrades | Stays fast
The one place workspaces genuinely win: short-lived PR preview environments spun up inside an already-isolated non-prod account. Nobody's applying prod changes there by mistake because prod credentials don't exist in that context. Everywhere else — especially once prod lives in its own AWS account, which is standard practice in any AWS Landing Zone setup — workspaces are the wrong tool, because they can't switch which account credentials point to. That's not a workaround gap, it's by design.
My pick
Directory-per-environment with separate AWS accounts and separate backends, by default, for dev/staging/prod. Workspaces only for short-lived feature or preview stacks inside an account that's already isolated from anything that matters.
The duplicated boilerplate you take on with separate backends is a small, known, one-time cost. The cost of a shared-state incident wiping or corrupting prod is unbounded and happens at the worst possible time — usually during a release, usually under pressure, usually right when your on-call engineer is already dealing with something else. I stopped recommending shared-backend workspaces for prod/staging separation after watching a team spend a weekend reconstructing state from S3 versioning because a lock got orphaned mid-apply in a shared table.
One more thing worth planning for honestly: migrating off workspaces later is not free. It means terraform state mv or a full re-init with -migrate-state, run carefully against real infrastructure. That's a far riskier operation to perform under deadline pressure than it would have been to set up the separate-backend pattern correctly on day one. If you're setting up multi-env Terraform right now, pay the setup cost up front — see the Terraform S3 backend docs and the official guidance on workspaces before you commit either way. For more patterns on structuring infrastructure-as-code pipelines, check the related posts on kuryzhev.cloud.
Top comments (0)