Terraform Workspaces and Remote State – T11
Hey there! Welcome back to our Terraform adventure. Today, we’re going to solve a big problem that every real project faces: how do you manage multiple environments without going crazy?
Think about it – you need development, staging, and production environments. They’re similar but not identical. How do you keep them organized? How do you make sure your team doesn’t accidentally break production while testing? That’s what we’re solving today!
Managing multiple environments with Terraform Like a Pro
The Multiple Environment Problem
Let’s say you’ve built a great web application infrastructure. Now you need:
Development Environment:
- Small servers (save money)
- Relaxed security (for testing)
- Temporary data (can be deleted)
Staging Environment:
- Medium servers (test performance)
- Production-like security
- Stable data (for testing)
Production Environment:
- Large servers (handle real users)
- Maximum security
- Permanent data (never lose this!)
The messy way would be to copy your Terraform code three times:
web-app-dev/
├── main.tf (dev configuration)
├── variables.tf
└── outputs.tf
web-app-staging/
├── main.tf (staging configuration - mostly copied)
├── variables.tf
└── outputs.tf
web-app-prod/
├── main.tf (prod configuration - mostly copied)
├── variables.tf
└── outputs.tf
But what happens when you need to fix a bug or add a feature? You have to remember to update all three copies. That’s a recipe for disaster!
What Are Terraform Workspaces?
Workspaces are like different “containers” for your infrastructure. Each workspace has its own state file, but they all use the same Terraform code.
Think of it like having one blueprint for a house, but you can build that house in different neighborhoods (workspaces). Each house is separate, but they’re built from the same plans.
Same Terraform Code → Different Workspaces → Different Infrastructure
main.tf ────┬─→ dev workspace ────→ dev environment
├─→ staging workspace ─→ staging environment
└─→ prod workspace ────→ prod environment
Understanding State Files and Workspaces
Without workspaces: You have one state file (terraform.tfstate
) that tracks one set of infrastructure.
With workspaces: Each workspace gets its own state file:
terraform.tfstate.d/dev/terraform.tfstate
terraform.tfstate.d/staging/terraform.tfstate
terraform.tfstate.d/prod/terraform.tfstate
This means you can have completely separate infrastructure that doesn’t interfere with each other!
Your First Workspace
Let’s start simple. Create this basic configuration:
File: main.tf
variable "environment" {
type = string
description = "Environment name"
default = "dev"
}
locals {
# Get current workspace name
current_workspace = terraform.workspace
# Environment-specific settings
env_config = {
dev = {
instance_count = 1
instance_size = "small"
backup_enabled = false
}
staging = {
instance_count = 2
instance_size = "medium"
backup_enabled = true
}
prod = {
instance_count = 3
instance_size = "large"
backup_enabled = true
}
}
# Get config for current workspace
config = local.env_config[local.current_workspace]
}
# Create environment-specific configuration
resource "local_file" "env_config" {
filename = "${local.current_workspace}-environment.json"
content = jsonencode({
workspace = local.current_workspace
instance_count = local.config.instance_count
instance_size = local.config.instance_size
backup_enabled = local.config.backup_enabled
created_at = timestamp()
})
}
# Create workspace-specific server configs
resource "local_file" "server_configs" {
count = local.config.instance_count
filename = "${local.current_workspace}-server-${count.index + 1}.conf"
content = <<-EOF
# Server Configuration
Environment: ${local.current_workspace}
Server: ${count.index + 1}
Size: ${local.config.instance_size}
Backup: ${local.config.backup_enabled ? "enabled" : "disabled"}
# Generated on ${timestamp()}
EOF
}
output "workspace_info" {
value = {
current_workspace = local.current_workspace
config = local.config
servers_created = local.config.instance_count
files_created = concat(
[local_file.env_config.filename],
[for server in local_file.server_configs : server.filename]
) } }
Now let’s see workspaces in action:
# See current workspace (starts with 'default')
terraform workspace show
# List all workspaces
terraform workspace list
# Create and switch to dev workspace
terraform workspace new dev
terraform apply
# Create and switch to staging workspace
terraform workspace new staging
terraform apply
# Create and switch to prod workspace
terraform workspace new prod
terraform apply
# Switch between workspaces
terraform workspace select dev
terraform workspace select staging
terraform workspace select prod
What’s terraform.workspace
? It’s a special variable that always contains the name of the current workspace. Super useful for making environment-specific decisions!
Workspace Commands You Need to Know
# List all workspaces (* shows current)
terraform workspace list
# Create new workspace
terraform workspace new WORKSPACE_NAME
# Switch to existing workspace
terraform workspace select WORKSPACE_NAME
# Show current workspace
terraform workspace show
# Delete workspace (must be empty)
terraform workspace delete WORKSPACE_NAME
Try these commands and see how each workspace has its own separate state!
Why Workspaces Are Awesome
Isolation: Each environment is completely separate. You can’t accidentally break production while working on development.
Same Code: One set of Terraform files works for all environments. Fix a bug once, it’s fixed everywhere.
Easy Switching: Jump between environments with one command.
Environment-Specific Logic: Use terraform.workspace
to make different choices for different environments.
State Separation: Each workspace has its own state file, so they don’t interfere with each other.
The Remote State Problem
Local workspaces are great for learning, but real teams need remote state. Here’s why:
Team Collaboration: Multiple people need to work on the same infrastructure.
State Security: State files contain sensitive information and shouldn’t live on laptops.
State Backup: If your laptop crashes, you don’t want to lose your infrastructure state.
State Locking: Prevent two people from running Terraform at the same time.
Audit Trail: Track who changed what and when.
Remote State Backends
Terraform can store state in many remote locations:
Popular options:
- AWS S3 (most common for AWS users)
- Azure Storage (for Azure users)
- Google Cloud Storage (for GCP users)
- Terraform Cloud (HashiCorp’s hosted service)
- Consul (for advanced users)
Let’s focus on S3 since it’s the most popular.
Setting Up Remote State with S3
Step 1: Create S3 bucket and DynamoDB table
You need these AWS resources first (create them manually or with Terraform):
# This is a one-time setup - create these first
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-company-terraform-state-12345" # Must be globally unique
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-state-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Step 2: Configure backend in your project
Add this to your main Terraform configuration:
terraform {
backend "s3" {
bucket = "my-company-terraform-state-12345"
key = "web-app/terraform.tfstate"
region = "us-west-2"
dynamodb_table = "terraform-state-locks"
encrypt = true
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-west-2"
}
Step 3: Initialize with remote backend
# This moves your state to S3
terraform init
# Now your state is stored remotely!
terraform workspace list
terraform workspace new staging
terraform apply
Remote State with Workspaces
Here’s the beautiful part – remote state works perfectly with workspaces:
S3 Bucket Structure:
my-terraform-state/
├── web-app/
│ ├── env:/default/terraform.tfstate
│ ├── env:/dev/terraform.tfstate
│ ├── env:/staging/terraform.tfstate
│ └── env:/prod/terraform.tfstate
Each workspace gets its own state file in S3, but they’re all organized under your project!
Advanced Remote State Configuration
For real projects, you want more sophisticated organization:
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "projects/web-app/${terraform.workspace}/terraform.tfstate"
region = "us-west-2"
dynamodb_table = "terraform-state-locks"
encrypt = true
# Optional: Use different AWS profiles per environment
profile = terraform.workspace == "prod" ? "prod-account" : "dev-account"
}
}
This creates a structure like:
projects/web-app/dev/terraform.tfstate
projects/web-app/staging/terraform.tfstate
projects/web-app/prod/terraform.tfstate
State Locking in Action
Without locking:
Person A: terraform apply ← starts
Person B: terraform apply ← starts at same time
Result: 💥 CONFLICT! State corruption!
With locking:
Person A: terraform apply ← starts, gets lock
Person B: terraform apply ← waits for lock
Person A: finishes ← releases lock
Person B: now can proceed ← gets lock and continues
DynamoDB provides the locking mechanism automatically when you specify the dynamodb_table
.
Environment-Specific Configurations
Here’s a practical pattern for handling different environment needs:
locals {
# Environment-specific configurations
environments = {
dev = {
instance_type = "t3.micro"
min_size = 1
max_size = 2
enable_backup = false
enable_monitoring = false
domain_prefix = "dev"
}
staging = {
instance_type = "t3.small"
min_size = 2
max_size = 4
enable_backup = true
enable_monitoring = true
domain_prefix = "staging"
}
prod = {
instance_type = "t3.medium"
min_size = 3
max_size = 10
enable_backup = true
enable_monitoring = true
domain_prefix = "www"
}
}
# Get current environment config
env = local.environments[terraform.workspace]
# Common settings
common_tags = {
Environment = terraform.workspace
Project = "web-app"
ManagedBy = "terraform"
LastModified = timestamp()
}
}
# Use environment-specific settings
resource "local_file" "app_config" {
filename = "${terraform.workspace}-app-config.json"
content = jsonencode({
environment = terraform.workspace
# Environment-specific settings
instance_type = local.env.instance_type
scaling = {
min_size = local.env.min_size
max_size = local.env.max_size
}
features = {
backup = local.env.enable_backup
monitoring = local.env.enable_monitoring
}
# Computed values
domain_name = "${local.env.domain_prefix}.mycompany.com"
# Common settings
tags = local.common_tags
})
}
# Create backup configuration only if enabled
resource "local_file" "backup_config" {
count = local.env.enable_backup ? 1 : 0
filename = "${terraform.workspace}-backup.conf"
content = <<-EOF
# Backup configuration for ${terraform.workspace}
retention_days = ${terraform.workspace == "prod" ? 90 : 30}
backup_frequency = ${terraform.workspace == "prod" ? "hourly" : "daily"}
# Environment: ${terraform.workspace}
# Generated: ${timestamp()}
EOF
}
output "environment_summary" {
value = {
workspace = terraform.workspace
instance_type = local.env.instance_type
scaling_range = "${local.env.min_size}-${local.env.max_size}"
backup_enabled = local.env.enable_backup
monitoring = local.env.enable_monitoring
domain_name = "${local.env.domain_prefix}.mycompany.com"
files_created = concat(
[local_file.app_config.filename],
local.env.enable_backup ? [local_file.backup_config[0].filename] : []
)
}
}
Team Collaboration Best Practices
1. Shared Remote State Everyone on the team uses the same S3 bucket for state storage.
2. Clear Workspace Naming Use consistent workspace names that everyone understands:
dev
(ordevelopment
)staging
(orstage
)prod
(orproduction
)
3. Workspace Ownership
- dev: Developers can create/destroy freely
- staging: Shared by team, coordinate changes
- prod: Protected, require approval for changes
4. State File Organization
terraform-state-bucket/
├── project-web-app/
├── project-api/
├── project-database/
└── shared-infrastructure/
5. Access Controls Different team members get different permissions:
- Developers: Full access to dev, read-only on staging/prod
- DevOps: Full access to all environments
- QA: Read access to staging for testing validation
Workspace Security Considerations
Separate AWS Accounts For maximum security, use different AWS accounts:
terraform {
backend "s3" {
bucket = "terraform-state"
key = "web-app/terraform.tfstate"
region = "us-west-2"
# Use different accounts based on workspace
role_arn = terraform.workspace == "prod" ?
"arn:aws:iam::PROD-ACCOUNT:role/TerraformRole" :
"arn:aws:iam::DEV-ACCOUNT:role/TerraformRole"
}
}
Environment-Specific Secrets
locals {
# Different secrets per environment
database_passwords = {
dev = "simple_dev_password"
staging = var.staging_db_password # From environment variable
prod = var.prod_db_password # From secure vault
}
db_password = local.database_passwords[terraform.workspace]
}
Common Workspace Patterns
Pattern 1: Feature Branches
# Create workspace for feature development
terraform workspace new feature-user-auth
# Work on feature
terraform workspace select feature-user-auth
terraform apply
# When done, clean up
terraform destroy
terraform workspace select default
terraform workspace delete feature-user-auth
Pattern 2: Developer Workspaces
# Each developer gets their own workspace
terraform workspace new dev-alice
terraform workspace new dev-bob
terraform workspace new dev-charlie
Pattern 3: Environment Promotion
# Deploy to dev first
terraform workspace select dev
terraform apply
# Test in dev, then promote to staging
terraform workspace select staging
terraform apply
# Test in staging, then promote to prod
terraform workspace select prod
terraform apply
Troubleshooting Common Issues
Issue: “Workspace doesn’t exist”
Error: Workspace "staging" doesn't exist.
Solution:
terraform workspace new staging
Issue: “State lock timeout”
Error: Error acquiring the state lock
Solution:
# Someone else is running terraform, wait for them to finish
# Or if process crashed, force unlock (dangerous!)
terraform force-unlock LOCK_ID
Issue: “Backend configuration changed”
Error: Backend configuration changed
Solution:
terraform init -reconfigure
Issue: “Wrong workspace”
# Always check current workspace before applying!
terraform workspace show
# Switch to correct workspace
terraform workspace select prod
Workspace Limitations and Alternatives
Workspace limitations:
- All workspaces use the same Terraform code
- Can’t have drastically different architectures
- Workspace names are global per backend
- Limited to one backend per configuration
When NOT to use workspaces:
- Different cloud providers: Use separate configurations
- Completely different architectures: Use separate projects
- Different teams: Use separate state buckets
- Different compliance requirements: Use separate everything
Alternative approaches:
# Separate directories approach
projects/
├── web-app-dev/
├── web-app-staging/
└── web-app-prod/
# Separate repositories approach
web-app-dev-terraform/
web-app-staging-terraform/
web-app-prod-terraform/
# Terragrunt approach (advanced tool)
environments/
├── dev/
├── staging/
└── prod/
Quick Reference
Workspace Commands:
terraform workspace list # Show all workspaces
terraform workspace show # Show current workspace
terraform workspace new NAME # Create new workspace
terraform workspace select NAME # Switch to workspace
terraform workspace delete NAME # Delete workspace
Remote State Backend:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "project/terraform.tfstate"
region = "us-west-2"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
Environment-Specific Logic:
locals {
is_prod = terraform.workspace == "prod"
config = local.environments[terraform.workspace]
}
What’s Next?
Amazing work! You’ve learned how to manage multiple environments professionally:
Workspace Fundamentals:
- ✅ Understanding workspace isolation and benefits
- ✅ Creating and switching between workspaces
- ✅ Using
terraform.workspace
for environment-specific logic - ✅ Organizing workspace-specific configurations
Remote State Management:
- ✅ Setting up S3 backend with DynamoDB locking
- ✅ Team collaboration with shared state
- ✅ State security and access control
- ✅ Troubleshooting common state issues
Professional Practices:
- ✅ Environment-specific configurations and patterns
- ✅ Workspace naming and organization strategies
- ✅ Security considerations for production environments
- ✅ When to use workspaces vs alternative approaches
In our next post, we’ll explore Terraform Best Practices and Project Organization – how to structure large, complex Terraform projects for maintainability and team collaboration. You’ll learn:
- Project structure and file organization
- Code style and naming conventions
- Testing strategies for Terraform
- CI/CD integration patterns
- Security best practices
- Performance optimization techniques
The workspace and state management skills you’ve learned today are essential foundation for building production-ready infrastructure!
Ready to learn how to organize and maintain large Terraform projects like a pro? The next post will teach you enterprise-level best practices!