Why this choice matters
Hey, want to stop sweating every prod apply?
The way you split dev, staging, and prod in Terraform decides how much damage a single mistake can do. Get it right and a bad apply is annoying. Get it wrong and the same command that fixes dev can wreck prod, because they share too much.
It is a decision you make early and live with
Most teams pick an environment strategy on day one, before they have a prod worth protecting, and never revisit it. By the time it hurts, there is real state to migrate. So it is worth understanding the trade-off now.
The problem with workspaces at scale
What’s the issue?
Terraform workspaces share a single backend and split your state by name. You run terraform workspace select prod, and the same root config now points at the prod state. The config is identical across environments; only the state differs.
That is exactly what makes them risky. Every environment runs the same .tf files, so there is no room for prod to legitimately differ from dev, and switching environments is a single command with no folder to remind you where you are.
The real-world consequence
The classic incident is running terraform apply thinking you are in dev while the workspace is still set to prod. Nothing in the file tells you which one you are on. Add per-environment differences (a bigger instance in prod, an extra region) and you start bolting count and var.environment conditionals into shared code until it is a tangle nobody wants to touch.
A folder per environment
Here’s how to fix it
Give each environment its own directory with its own backend and its own state. envs/dev/, envs/staging/, envs/prod/, each initialized separately. Now the blast radius stops at one folder. You cannot accidentally apply prod from the dev directory, and each environment can differ honestly without conditionals smeared through shared code.
Implementing it without copy-paste
The obvious objection is duplication. If every folder has its own config, are you not repeating yourself? That is what modules are for. The real resources live in modules/, and each environment folder is a thin wrapper that calls them with its own variables.
Tools and platforms
Terragrunt is the common answer for keeping the directory approach DRY. It generates the backend config per environment and lets each folder stay tiny, so you get isolated state without copy-pasting backend blocks. Plain Terraform with modules works too; Terragrunt just removes the last of the boilerplate.
Quick implementation steps
Quick takeaways
- Workspaces: one backend, state split by name, config shared. Fine when small.
- Directories: one folder and one state per environment. Scales because the blast radius is contained.
- Migrate one environment at a time, dev first and prod last.
- Trust the move only when
terraform planshows zero changes.
Migrating without breaking things
You do not have to do this all at once. Move one environment at a time, starting with dev, using state pull to grab the current state and state push (or targeted import) to load it into the new per-folder backend.
Benefits you feel quickly
Why it helps
The payoff is a smaller blast radius. When prod has its own folder and its own state, there is no single command that flips you into prod by accident, and a mistake in the dev folder simply cannot reach it.
Cleaner diffs and real differences
Because environments no longer share one config, per-environment differences become explicit and readable instead of hidden behind conditionals. Your prod folder says what prod is, in plain HCL, and a code review shows exactly which environment a change touches.
What’s your approach?
Community discussion
What’s your take? Are you still running everything through workspaces, or have you moved to a folder per environment? I am curious where the line is for other teams.
Share your experience
If you have migrated off workspaces, I would love to hear how the state pull / state push dance went and whether Terragrunt earned its place in your setup.









