In real-world infrastructure, you rarely manage a single environment. You need separate copies for development, staging, and production — each isolated, each with its own configuration. Terraform workspaces and environment-aware directory layouts give you the tools to manage this cleanly.
1. Learning Objectives
By the end of this lesson, you will be able to:
- Explain the difference between Terraform workspaces and directory-based environment separation
- Create, switch between, and delete Terraform workspaces using CLI commands
- Use
terraform.workspaceexpressions to make configurations environment-aware - Design a directory layout for managing dev, staging, and production environments
- Configure remote backends with workspace-specific state files
- Apply environment-specific variable files without duplication
2. Why This Matters
Imagine you have built a beautiful multi-tier application: a load balancer, several web servers, a database, and a caching layer. Your Terraform configuration deploys it perfectly — onto your production account. But where do you test changes? Where do you try that new database version before rolling it out to users?
Without environment management, teams resort to copy-pasting entire directories (stage-1, stage-2, prod-v2) or manually editing state files. Both approaches are fragile, error-prone, and impossible to audit. Terraform workspaces and environment-aware layouts solve this problem cleanly, safely, and scalably.
3. Core Concepts
3.1 What is a Terraform Workspace?
A workspace is a named instance of your Terraform state. When you run terraform init, you start in the default workspace. Every workspace maintains its own state file — completely isolated from every other workspace — while sharing the same configuration code.
Think of workspaces as separate "staging areas" for your infrastructure. The same configuration file, deployed in different workspaces, creates separate copies of your resources in different environments.
# List all workspaces
terraform workspace list
# Create a new workspace
terraform workspace new staging
# Switch to a workspace
terraform workspace select production
# Show current workspace
terraform workspace show
3.2 Directory-Based Environments
Workspaces work well for simple dev/staging/prod separation. But for complex deployments where environments differ significantly (different regions, different instance types, different VPC CIDR blocks), many teams prefer a directory-based layout:
infrastructure/
├── global/ # Shared resources (IAM, S3 backend)
│ └── main.tf
├── modules/ # Reusable modules
│ ├── vpc/
│ ├── ecs-service/
│ └── rds/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ └── production/
│ ├── main.tf
│ ├── variables.tf
│ └── terraform.tfvars
└── backends/
└── dev-backend.tf
The directory layout gives each environment its own root module, letting you vary provider configurations, Terraform versions, and even backend types between environments. Workspaces are simpler for homogeneous environments (same config, different counts).
3.3 The terraform.workspace Expression
Terraform provides a built-in terraform.workspace expression that resolves to the current workspace name at runtime. Use it to vary resource names, instance sizes, and tags without changing code when switching workspaces:
# environment-config.tf
variable "instance_type_map" {
default = {
default = "t3.micro"
staging = "t3.small"
production = "t3.large"
}
}
locals {
env_name = terraform.workspace
instance_type = lookup(
var.instance_type_map,
terraform.workspace,
var.instance_type_map["default"]
)
name_prefix = "${terraform.workspace}-app"
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = local.instance_type
tags = {
Name = "${local.name_prefix}-web"
Environment = local.env_name
ManagedBy = "Terraform"
}
}
4. Hands-On Practice
4.1 Setting Up Workspaces
Create a simple Terraform project that deploys an AWS EC2 instance. We will use workspaces to manage separate dev and staging instances.
# Create project directory
mkdir ~/terraform-workspaces-demo
cd ~/terraform-workspaces-demo
# Initialize the project
cat > main.tf << 'EOF'
terraform {
required_version = ">= 1.5"
backend "s3" {
bucket = "my-company-terraform-state"
key = "workspaces-demo/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "${terraform.workspace}-web-server"
Environment = terraform.workspace
}
}
EOF
terraform init
4.2 Creating and Using Workspaces
# Create the dev workspace
terraform workspace new dev
terraform workspace show
# Output: dev
# Deploy to dev
terraform plan -out=dev.tfplan
terraform apply dev.tfplan
# Now create the staging workspace
terraform workspace new staging
# Deploy to staging
terraform plan -out=staging.tfplan
terraform apply staging.tfplan
# List all workspaces
terraform workspace list
# Output:
# default
# dev
# * staging
# Switch back to dev
terraform workspace select dev
terraform show
Notice that terraform workspace show returns the current workspace name, and the asterisk in terraform workspace list marks your active workspace. Each workspace maintains its own state file in the S3 backend — completely isolated.
4.3 Environment-Specific Variable Files
Workspaces share the same terraform.tfvars file. To use per-environment values, name your variable files with the workspace name:
# Create env-specific variable files
cat > dev.tfvars << 'EOF'
instance_type = "t3.micro"
desired_count = 1
environment = "dev"
EOF
cat > staging.tfvars << 'EOF'
instance_type = "t3.small"
desired_count = 2
environment = "staging"
EOF
cat > production.tfvars << 'EOF'
instance_type = "t3.large"
desired_count = 5
environment = "production"
EOF
# Apply with env-specific vars
terraform workspace select dev
terraform apply -var-file="dev.tfvars"
terraform workspace select staging
terraform apply -var-file="staging.tfvars"
4.4 Remote Backend with Workspace Key Prefix
When using a remote backend, Terraform automatically namespaces state files by workspace. With the S3 backend configured above, the states will be stored as:
# S3 bucket structure (auto-generated by workspace key)
s3://my-company-terraform-state/
workspaces-demo/
terraform.tfstate # default workspace
terraform.tfstate:dev # dev workspace
terraform.tfstate:staging # staging workspace
# Using workspace_key_prefix for multi-project backends
terraform {
backend "s3" {
bucket = "my-company-terraform-state"
key = "networking/terraform.tfstate"
workspace_key_prefix = "environments"
region = "us-east-1"
}
}
# Result: environments/dev/networking/terraform.tfstate
The workspace_key_prefix parameter (default: env:) adds a directory layer in the state path. Setting it to a meaningful prefix like environments makes your state bucket self-documenting.
4.5 Destroying Environment Resources
# Switch to the workspace you want to destroy
terraform workspace select dev
# Destroy resources in this workspace
terraform destroy -auto-approve
# Delete the workspace (only works if no resources remain)
terraform workspace delete dev
# Force delete a workspace with resources
terraform workspace delete -force dev
5. Common Errors & Solutions
Error 1: "Workspace name is required when locking state"
When using a remote backend that supports workspaces (S3, GCS, AzureRM), some operations require an explicit workspace. The fix: switch to the correct workspace first, or pass TF_WORKSPACE=staging terraform apply as an environment variable.
Error 2: "Current workspace is already deleted"
If you delete the workspace you are currently in, you must switch to another workspace before running any command:
terraform workspace select default
Error 3: Resource naming conflicts across workspaces
If two workspaces create resources with the same name in the same AWS account, you get duplicate name errors. Solution: always include terraform.workspace in resource names and tags as shown in section 3.3.
Error 4: Accidental production apply while in wrong workspace
This is the most dangerous workspace mistake. Protect against it with a pre-condition check:
# safety_check.tf
locals {
env_checks = {
dev = true
staging = true
production = regex("^prod", terraform.workspace) ? true : false
default = false
}
}
resource "null_resource" "environment_check" {
lifecycle {
precondition {
condition = contains(keys(local.env_checks), terraform.workspace)
error_message = "Unknown workspace: ${terraform.workspace}. Must be dev, staging, or production."
}
}
}
Error 5: State lock conflicts in CI/CD pipelines
When multiple CI/CD runs target the same workspace simultaneously, they compete for the state lock. Solution: use -lock-timeout=5m to wait and retry automatically:
terraform apply -lock-timeout=5m -auto-approve
Or queue CI/CD runs per environment to avoid parallel conflicts entirely.
6. Summary Checklist
- Understand the two environment strategies: workspaces (simple) vs directory-based (complex)
- List, create, select, and delete workspaces with
terraform workspace *commands - Use
terraform.workspacein resource names, tags, and variable lookups - Organize variable files as
dev.tfvars,staging.tfvars,production.tfvars - Configure
workspace_key_prefixfor readable state file paths - Add pre-condition checks to prevent applying to the wrong workspace
- Use
-lock-timeoutto handle concurrent state locks in CI/CD - Delete workspaces after destroying their resources to keep the state bucket clean
7. Practice Exercise
Challenge: Multi-Environment Web Application
Create a Terraform configuration for a three-environment deployment:
- Create three workspaces:
dev,staging,production - Each workspace deploys an EC2 instance with an environment-specific name tag
- Create per-environment variable files setting different instance types:
t3.nanofor dev,t3.microfor staging,t3.smallfor production - Add a
user_datascript that writes the environment name to/etc/environment - Use an S3 backend with
workspace_key_prefix = "environments" - Add a pre-condition guard that rejects an unknown workspace name before creating resources
Bonus: Add a helm_release resource that deploys an Nginx ingress chart with values varying by workspace.
8. Next Steps
You now have a solid understanding of Terraform workspaces and environment management. In the next lesson, we will explore Terraform Security & Secrets Management — how to handle sensitive data in state files, integrate with HashiCorp Vault, encrypt state at rest, and follow least-privilege principles for your Terraform service accounts.
Until then, apply what you have learned: set up workspaces for your own projects and practice the deploy-destroy cycle across dev, staging, and production environments. The muscle memory of workspace new → select → plan → apply will serve you well in production.
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment