Terraform Workspaces: Multi-Environment Deployments
Terraform Workspaces: Multi-Environment Deployments
Manage dev, staging, and production with Terraform workspaces — commands, patterns, and best practices for environment isolation.
Table of Contents
- What Are Workspaces
- Workspace Commands
- Environment Pattern
- Conditional Resources
- Limitations
- Alternative: Directory Structure
- Best Practices
What Are Workspaces
Terraform workspaces let you manage multiple states within a single configuration. Each workspace gets its own state file, but shares the same .tf code.
Think of it as: same recipe, different kitchens.
terraform.workspace = "dev" → terraform.tfstate.d/dev/terraform.tfstate
terraform.workspace = "staging" → terraform.tfstate.d/staging/terraform.tfstate
terraform.workspace = "prod" → terraform.tfstate.d/prod/terraform.tfstate
Workspace Commands
# List workspaces (default is "default")
terraform workspace list
# Create a workspace
terraform workspace new staging
# Switch workspaces
terraform workspace select dev
# Show current workspace
terraform workspace show
# Delete a workspace (state must be empty)
terraform workspace delete staging
Environment Pattern
Simple Setup
variable "environment" {
type = string
default = "dev"
}
locals {
env = terraform.workspace == "default" ? "dev" : terraform.workspace
# Environment-specific config
instance_count = {
dev = 1
staging = 2
prod = 5
}
vm_size = {
dev = "Standard_B2s"
staging = "Standard_D2s_v5"
prod = "Standard_D4s_v5"
}
}
resource "azurerm_linux_virtual_machine" "app" {
name = "app-${local.env}"
size = local.vm_size[local.env]
instance_count = local.instance_count[local.env]
resource_group_name = "myapp-${local.env}-rg"
# ...
}
Per-Environment Variable Files
environments/
├── dev.tfvars
├── staging.tfvars
└── prod.tfvars
# environments/dev.tfvars
instance_count = 1
vm_size = "Standard_B2s"
enable_ha = false
# environments/prod.tfvars
instance_count = 5
vm_size = "Standard_D4s_v5"
enable_ha = true
Apply:
terraform workspace select prod
terraform apply -var-file="environments/prod.tfvars"
Backend Configuration per Workspace
terraform {
backend "azurerm" {
resource_group_name = "tfstate-rg"
storage_account_name = "tfstatestg"
container_name = "tfstate"
key = "terraform.tfstate" # auto-suffixed by workspace
}
}
State paths become:
terraform.tfstate.d/dev/terraform.tfstateterraform.tfstate.d/prod/terraform.tfstate
Conditional Resources
resource "azurerm_monitor_autoscale_setting" "prod" {
count = terraform.workspace == "prod" ? 1 : 0
# ... only exists in production
}
resource "azurerm_key_vault_secret" "dev_override" {
count = terraform.workspace == "dev" ? 1 : 0
# ... only in dev
}
locals {
# Scale resources by environment
node_count = terraform.workspace == "prod" ? 10 : 1
# Different VM images per environment
image_reference = terraform.workspace == "prod" ? {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts-gen2"
} : {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-focal"
sku = "20_04-lts-gen2"
}
}
Limitations
1. Same Backend for All Environments
All workspaces share the same backend config — you can't use different state backends for dev vs prod.
2. No Workspace-Scoped Access Control
Anyone with backend access can see all workspaces. Prod and dev share the same permissions.
3. Easy to Apply to Wrong Workspace
terraform apply # WRONG — might hit prod
terraform workspace select dev && terraform apply # correct
4. Drift Between Environments
Code is shared but states diverge — a prod hotfix that isn't in dev causes confusion.
Alternative: Directory Structure
For production use, many teams prefer separate directories instead of workspaces:
infra/
├── modules/
│ └── web-app/ # shared module
├── environments/
│ ├── dev/
│ │ ├── main.tf # calls modules
│ │ └── backend.tf # separate state backend
│ ├── staging/
│ └── prod/
│ ├── main.tf
│ └── backend.tf # different subscription/storage
# environments/dev/backend.tf — separate state
terraform {
backend "azurerm" {
resource_group_name = "tfstate-dev-rg"
storage_account_name = "tfstatedev"
container_name = "tfstate"
key = "dev.tfstate"
}
}
# environments/prod/backend.tf — completely different state
terraform {
backend "azurerm" {
resource_group_name = "tfstate-prod-rg"
storage_account_name = "tfstateprod"
container_name = "tfstate"
key = "prod.tfstate"
}
}
When to Use Which
| Workspaces | Directory Structure |
|---|---|
| Quick dev/staging parity | Production isolation |
| Small team, simple setup | Multiple environments |
| Same backend OK | Different subscriptions/accounts |
| Learning Terraform | Enterprise/production |
Best Practices
1. Always Check Workspace Before Apply
# Safety check in CI/CD
CURRENT_WS=$(terraform workspace show)
if [ "$CURRENT_WS" != "dev" ]; then
echo "Not in dev workspace! Current: $CURRENT_WS"
exit 1
fi
terraform apply
2. Lock State Per Workspace
# In backend config, enable state locking
terraform {
backend "azurerm" {
# ...
use_azuread_auth = true
}
}
3. Different Subscriptions? Use Directories
# prod uses a different Azure subscription — needs different provider
provider "azurerm" {
subscription_id = var.subscription_id # different per environment
}
4. Name Resources by Workspace
locals {
env = terraform.workspace
}
resource "azurerm_resource_group" "main" {
name = "myapp-${local.env}-rg" # myapp-dev-rg, myapp-prod-rg
location = "uksouth"
}
5. Don't Use default Workspace
terraform workspace new dev
terraform workspace select dev
terraform apply
# Don't work in "default" — it's ambiguous
Related Articles
Last Updated: October 2026
Author: CloudOpsGuide Team
Difficulty: Intermediate
Estimated Reading Time: 12 minutes