Terraform workspaces let you manage multiple instances of the same infrastructure configuration — such as development, staging, and production environments — without duplicating code. Each workspace maintains its own state file while sharing the same configuration. This guide covers using workspaces effectively for multi-environment infrastructure management.
Understanding Workspaces
- Default workspace: Every Terraform project starts with a "default" workspace
- Named workspaces: Create additional workspaces for different environments
- Isolated state: Each workspace has its own state file — changes in one don't affect others
- Shared code: All workspaces share the same Terraform configuration files
Creating and Switching Workspaces
# List workspaces
terraform workspace list
# Create new workspaces
terraform workspace new dev
terraform workspace new staging
terraform workspace new production
# Switch between workspaces
terraform workspace select production
# Show current workspace
terraform workspace show
# Delete a workspace (must not be current)
terraform workspace select default
terraform workspace delete dev
Environment-Specific Configuration
# variables.tf
variable "environment_configs" {
type = map(object({
server_count = number
server_type = string
enable_backups = bool
domain_prefix = string
}))
default = {
dev = {
server_count = 1
server_type = "cx22"
enable_backups = false
domain_prefix = "dev"
}
staging = {
server_count = 2
server_type = "cx32"
enable_backups = true
domain_prefix = "staging"
}
production = {
server_count = 3
server_type = "cx42"
enable_backups = true
domain_prefix = "www"
}
}
}
locals {
env = terraform.workspace
config = var.environment_configs[local.env]
common_labels = {
environment = local.env
managed_by = "terraform"
workspace = terraform.workspace
}
}
# main.tf — Configuration adapts to workspace
resource "hcloud_server" "web" {
count = local.config.server_count
name = "${local.env}-web-${count.index + 1}"
server_type = local.config.server_type
image = "ubuntu-24.04"
location = "nbg1"
backups = local.config.enable_backups
labels = merge(local.common_labels, {
role = "webserver"
index = count.index
})
}
resource "cloudflare_record" "web" {
count = local.config.server_count
zone_id = var.cloudflare_zone_id
name = "${local.config.domain_prefix}${count.index > 0 ? count.index + 1 : ""}"
type = "A"
content = hcloud_server.web[count.index].ipv4_address
proxied = local.env == "production"
}
# Database — only production gets a dedicated server
resource "hcloud_server" "database" {
count = local.env == "production" ? 1 : 0
name = "${local.env}-db-1"
server_type = "cx42"
image = "ubuntu-24.04"
backups = true
labels = merge(local.common_labels, { role = "database" })
}
output "web_ips" {
value = hcloud_server.web[*].ipv4_address
}
output "environment" {
value = local.env
}
Per-Workspace Variable Files
# Use .tfvars files per workspace
# environments/dev.tfvars
cloudflare_zone_id = "zone-id-for-dev"
ssh_key_ids = ["12345"]
alert_email = "dev-team@example.com"
# environments/production.tfvars
cloudflare_zone_id = "zone-id-for-prod"
ssh_key_ids = ["12345", "67890"]
alert_email = "ops@example.com"
# Apply with workspace-specific vars
terraform workspace select production
terraform apply -var-file="environments/$(terraform workspace show).tfvars"
# Or automate with a wrapper script
#!/bin/bash
# deploy.sh
WORKSPACE=$(terraform workspace show)
terraform apply -var-file="environments/${WORKSPACE}.tfvars" "$@"
Remote State with Workspaces
# Backend configuration — workspaces auto-create separate state paths
terraform {
backend "s3" {
bucket = "myorg-terraform-state"
key = "infrastructure/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
# State files will be stored as:
# env:/dev/infrastructure/terraform.tfstate
# env:/staging/infrastructure/terraform.tfstate
# env:/production/infrastructure/terraform.tfstate
workspace_key_prefix = "env"
}
}
CI/CD Integration
# .github/workflows/terraform.yml
name: Terraform Deploy
on:
push:
branches: [main, develop]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Determine workspace
id: workspace
run: |
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "workspace=production" >> $GITHUB_OUTPUT
else
echo "workspace=staging" >> $GITHUB_OUTPUT
fi
- name: Terraform Init
run: terraform init
- name: Select Workspace
run: terraform workspace select ${{ steps.workspace.outputs.workspace }}
- name: Terraform Plan
run: terraform plan -var-file="environments/${{ steps.workspace.outputs.workspace }}.tfvars" -out=tfplan
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply tfplan
When NOT to Use Workspaces
Workspaces are not always the right choice:
- Completely different infrastructure: If dev and prod have fundamentally different architectures, use separate directories
- Different providers or accounts: If environments run in different cloud accounts, separate configurations are cleaner
- Team isolation: If different teams manage different environments, separate repos provide better access control
Best Practices
- Never use the "default" workspace — create named workspaces for all environments
- Use workspace-specific .tfvars files for environment-specific values
- Add workspace guards for destructive operations:
prevent_destroy = local.env == "production" - Document which workspace maps to which environment in your README
- Use remote state with workspace-aware backends for team collaboration
- Consider the alternative: For complex environments, separate directories per environment may be cleaner