There's a specific moment every growing Terraform setup eventually hits: someone runs terraform apply, expecting staging, and it applies to production instead. Nobody did anything obviously wrong — the workspace was just selected incorrectly, or a variable file wasn't passed, or the wrong TF_WORKSPACE was set in a shell from an hour ago.
This isn't a training problem. It's a structural one, and it's fixable.
Why Terraform workspaces are the usual culprit
Workspaces let you reuse the same configuration across environments by switching a piece of implicit state (terraform workspace select production). That's exactly the problem: the difference between "safe" and "dangerous" is one command, run silently, with no diff shown before it takes effect.
Workspaces aren't wrong for every use case — they're reasonable for ephemeral, short-lived environments (a preview environment per pull request, for example). They're the wrong tool for the handful of environments — dev, staging, production — where a mistake is expensive.
A structure that makes the mistake harder
For environments that matter, we prefer:
- One root Terraform configuration per environment, each with its own backend configuration pointing at a distinct state file.
- Shared logic factored into modules, called by each environment's root config with explicit, reviewed variables.
- No implicit environment switching — moving between environments means changing directories, not running a workspace command.
environments/
production/
main.tf # calls modules/network, modules/compute
backend.tf # state: s3://.../production/terraform.tfstate
staging/
main.tf
backend.tf
This looks like more files than a workspace-based setup. It is. That's the point — the extra structure is what makes "which environment am I about to change" a question answered by which directory you're in, not by remembering to check.
CI should plan on every pull request, apply only on merge
Regardless of workspace or directory structure, the highest-leverage safety net is process: run terraform plan in CI on every pull request touching infrastructure, post the plan as a comment for review, and only run apply after merge to the branch that maps to that environment. This catches most mistakes — including the ones a better folder structure doesn't — before they touch real infrastructure.
State chaos is usually a naming and backend problem too
Beyond workspaces, the other common source of chaos is inconsistent state file naming and backend configuration copied by hand between environments. Standardize the backend key naming convention (<environment>/<component>/terraform.tfstate) and enforce it through a shared backend module or Terragrunt-style wrapper — don't leave it to memory.
None of this is exotic. It's the difference between infrastructure code that's forgiving of a Friday-afternoon mistake and infrastructure code that isn't.
Related services