Environments with Terraform Workspaces
I once ran terraform destroy in what I thought was the staging workspace. It was production. The workspace name in the prompt was easy to miss — (production) in parentheses between the resource count and the confirmation prompt. Twenty-three resources deleted before I hit Ctrl+C. Workspaces didn't cause the incident, but they didn't prevent it either.
Terraform workspaces create multiple state instances within a single backend configuration. terraform workspace select staging switches to a different state file; the same .tf files produce different infrastructure depending on variable values. They're lightweight environment isolation — useful for small projects, risky as a sole environment strategy for production systems.
How workspaces work
$ terraform workspace list
default
* staging
production
$ terraform workspace select production
The terraform.workspace variable returns the current workspace name:
locals {
environment = terraform.workspace
instance_type = {
staging = "t3.small"
production = "t3.large"
}
}
resource "aws_instance" "app" {
instance_type = local.instance_type[local.environment]
tags = { Environment = local.environment }
}
Same code, different resources per workspace.
Workspace-based variable files
terraform workspace select staging
terraform apply -var-file=staging.tfvars
terraform workspace select production
terraform apply -var-file=production.tfvars
Wrap in a script to prevent mismatched workspace/vars:
#!/bin/bash
ENV=$1
terraform workspace select "$ENV"
terraform apply -var-file="${ENV}.tfvars"
Workspaces vs separate directories
| Factor | Workspaces | Separate directories |
|---|---|---|
| Code duplication | None | Some — shared modules |
| Environment divergence | Awkward — conditionals | Natural |
| Access control | Shared backend | Separate backends, different IAM |
| Blast radius | One config change affects all | Scoped to one environment |
| Best for | Dev/staging identical shape | Production with different sizing |
Recommended hybrid approach
infra/
modules/ # Shared reusable modules
envs/
staging/
main.tf
backend.tf
staging.tfvars
production/
main.tf
backend.tf
production.tfvars
Both environments use the same modules. Root configs differ in variable values and possibly which modules are instantiated. State is fully isolated by backend key.
Safety guardrails
Prompt protection: Use lifecycle { prevent_destroy = true } on critical production resources.
Visual confirmation: Export PS1 to show current workspace: [tf:production].
CI/CD isolation: Separate GitHub environments with approval gates for production applies.
jobs:
apply-production:
environment: production
steps:
- run: terraform apply -var-file=production.tfvars -auto-approve
working-directory: envs/production
Production applies require explicit approval. Staging applies run automatically on merge.
Naming and tagging conventions across workspaces
When workspaces share an AWS account, enforce naming: ${var.project}-${terraform.workspace}-${var.resource}. Tags should include Environment = terraform.workspace for cost allocation. Without convention, dev and staging resources become indistinguishable in Cost Explorer and during incident response.
When to migrate off workspaces
If environments diverge in resources, provider versions, or AWS accounts, migrate to directory-per-environment with separate backend keys. Import existing resources into new state with terraform state mv or declarative import blocks. Document the migration in an ADR so teams do not reintroduce workspace sprawl for production.
Operational checklist for teams
Assign an owner for each recurring failure mode. Measure baseline before changes and compare after two sprints. Document the fix in the team wiki or runbook so the next engineer does not rediscover it. Schedule a quarterly review of metrics, tooling, and process — what reduced flakes or improved quality last quarter should be doubled down on; what did not work should be retired explicitly rather than left on the books.
Operational checklist for teams
Assign an owner for each recurring failure mode. Measure baseline before changes and compare after two sprints. Document the fix in the team wiki or runbook so the next engineer does not rediscover it. Schedule a quarterly review of metrics, tooling, and process — what reduced flakes or improved quality last quarter should be doubled down on; what did not work should be retired explicitly rather than left on the books.
Operational checklist for teams
Assign an owner for each recurring failure mode. Measure baseline before changes and compare after two sprints. Document the fix in the team wiki or runbook so the next engineer does not rediscover it. Schedule a quarterly review of metrics, tooling, and process — what reduced flakes or improved quality last quarter should be doubled down on; what did not work should be retired explicitly rather than left on the books.
Common production mistakes
Teams get workspaces environments wrong in predictable ways:
- Skipping failure-mode rehearsal — run a game day or fault injection exercise before peak traffic, not after the first outage.
- Missing correlation context — every error path should carry request, trace, or tenant identifiers so incidents are debuggable.
- Optimizing for demo, not steady state — load tests, cache warm-up, and cold-start paths matter more than local dev latency.
- Undocumented trade-offs — if you chose speed over strict correctness (or vice versa), write that down for the next engineer.
Terraform patterns for workspaces environments rot when emergency console edits never get codified, ignore_changes blocks multiply without documentation, and drift detection runs monthly instead of daily on production workspaces.
Debugging and triage workflow
When workspaces environments misbehaves in production, work top-down instead of guessing:
- Confirm scope — one tenant, region, or deployment stage? Narrow blast radius before deep diving.
- Check recent changes — deploys, flag flips, config pushes, and schema migrations in the last 24 hours.
- Compare golden signals — latency, error rate, saturation, and traffic for the affected surface vs. baseline.
- Reproduce minimally — smallest input or scenario that triggers the failure; capture traces/logs with correlation IDs.
- Fix forward or rollback — if rollback is faster than root-cause during incident, rollback first, postmortem second.
- Add a guard — alert, integration test, or circuit breaker so the same class of failure is caught earlier next time.
Document the timeline during triage. Future you (and on-call) will need timestamps, not just conclusions.
Resources
- Terraform workspaces documentation
- terraform.workspace reference
- Recommended environment strategy — HashiCorp
- Terraform directory structure best practices
- GitHub Environments for deployment protection
Frequently asked questions
Should I use Terraform workspaces or separate directories for environments?
Separate directories (envs/staging/, envs/production/) when environments differ significantly — different instance sizes, feature flags, or resource counts. Workspaces when environments are nearly identical and differ only in variable values (CIDR ranges, domain names). Most teams outgrow workspaces quickly because staging and production inevitably diverge. Directories scale better for teams with different access controls per environment.
Do Terraform workspaces provide security isolation?
No. Workspaces share the same backend bucket and code. A terraform workspace select production followed by terraform destroy in the wrong terminal destroys production. Workspace isolation is state isolation, not access control. For production safety, use separate state files in separate backend paths with IAM policies restricting who can write to production state.
How do I pass different values per workspace?
Use .tfvars files named per workspace (staging.tfvars, production.tfvars) and pass them with terraform apply -var-file=staging.tfvars. Or use workspace-specific variable defaults with terraform.workspace in conditionals. Avoid hardcoding environment logic scattered across resources — centralize in locals or a variables file.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →