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 (or development)
  • staging (or stage)
  • prod (or production)

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!

Akhilesh Mishra

Akhilesh Mishra

I am Akhilesh Mishra, a self-taught Devops engineer with 11+ years working on private and public cloud (GCP & AWS)technologies.

I also mentor DevOps aspirants in their journey to devops by providing guided learning and Mentorship.

Topmate: https://topmate.io/akhilesh_mishra/