Skip to main content

Terraform Workspaces — Manage Dev, Staging, and Production

· 6 min read
Goel Academy
DevOps & Cloud Learning Hub

Your Terraform code works perfectly in dev. Now you need the same infrastructure in staging and production, but with different instance sizes, different CIDR blocks, and different resource counts. You could copy the entire directory three times. Or you could use workspaces — Terraform's built-in mechanism for managing multiple instances of the same configuration with isolated state.

What Are Workspaces?

A workspace is an isolated state file within the same configuration directory. When you run terraform apply in a workspace, Terraform reads and writes to that workspace's own .tfstate file. The same .tf files produce different infrastructure depending on which workspace is active.

By default, every Terraform project starts in the default workspace. You have been using it all along without knowing.

# See the current workspace
terraform workspace show
# Output: default

# List all workspaces (* marks the active one)
terraform workspace list
# Output:
# * default

Creating and Switching Workspaces

# Create a new workspace (and switch to it)
terraform workspace new dev
# Output: Created and switched to workspace "dev"!

terraform workspace new staging
terraform workspace new production

# List workspaces
terraform workspace list
# Output:
# default
# dev
# staging
# * production

# Switch to an existing workspace
terraform workspace select dev
# Output: Switched to workspace "dev".

# Delete a workspace (must switch away first)
terraform workspace select default
terraform workspace delete dev
# Warning: this only deletes the state, not the real infrastructure

The terraform.workspace Variable

Inside your configuration, terraform.workspace is a string containing the name of the current workspace. This is the key to environment-specific behavior.

variable "instance_types" {
type = map(string)
default = {
dev = "t3.micro"
staging = "t3.small"
production = "t3.large"
}
}

variable "instance_counts" {
type = map(number)
default = {
dev = 1
staging = 2
production = 3
}
}

resource "aws_instance" "app" {
count = var.instance_counts[terraform.workspace]
ami = var.ami_id
instance_type = var.instance_types[terraform.workspace]

tags = {
Name = "app-${terraform.workspace}-${count.index + 1}"
Environment = terraform.workspace
}
}

Now terraform apply in the dev workspace creates 1 t3.micro, while the same code in production creates 3 t3.large instances.

Workspace-Based Configuration Patterns

A common pattern is to use a locals block that pulls environment-specific values from a map:

locals {
env_config = {
dev = {
instance_type = "t3.micro"
min_size = 1
max_size = 2
db_instance = "db.t3.micro"
multi_az = false
enable_logging = false
}
staging = {
instance_type = "t3.small"
min_size = 2
max_size = 4
db_instance = "db.t3.small"
multi_az = false
enable_logging = true
}
production = {
instance_type = "t3.large"
min_size = 3
max_size = 10
db_instance = "db.r6g.large"
multi_az = true
enable_logging = true
}
}

config = local.env_config[terraform.workspace]
}

resource "aws_db_instance" "main" {
engine = "postgres"
instance_class = local.config.db_instance
multi_az = local.config.multi_az
allocated_storage = 20

tags = {
Environment = terraform.workspace
}
}

resource "aws_autoscaling_group" "app" {
min_size = local.config.min_size
max_size = local.config.max_size
desired_capacity = local.config.min_size

launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}

tag {
key = "Environment"
value = terraform.workspace
propagate_at_launch = true
}
}

Workspace State Isolation

Each workspace gets its own state. With a local backend, Terraform stores workspace states in terraform.tfstate.d/<workspace>/:

project/
terraform.tfstate # "default" workspace state
terraform.tfstate.d/
dev/
terraform.tfstate # "dev" workspace state
staging/
terraform.tfstate # "staging" workspace state
production/
terraform.tfstate # "production" workspace state

With remote backends like S3, each workspace gets its own state key:

terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "infrastructure/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"

# Workspace states stored as:
# env:/dev/infrastructure/terraform.tfstate
# env:/staging/infrastructure/terraform.tfstate
# env:/production/infrastructure/terraform.tfstate
}
}

You can customize the workspace key prefix with the workspace_key_prefix argument (defaults to env:).

Workspaces vs Directory Structure

AspectWorkspacesDirectory Structure
Code duplicationNone — single codebaseSome — separate tfvars per env
State isolationAutomaticManual (separate backends)
Drift between envsImpossible (same code)Possible (files can diverge)
Env-specific resourcesConditional logic requiredSimple — just add/remove blocks
Provider config per envMust use conditionalsSeparate provider blocks
Team visibilityworkspace list shows allDirectory listing shows all
CI/CD complexityMust pass workspace nameMust specify directory path
Blast radiusCode change affects all envsChanges scoped to one env

Limitations of Workspaces

Workspaces are not a silver bullet. Here are the real limitations:

  1. No access control — anyone with backend access can switch to any workspace and run apply against production.

  2. Same backend — all workspaces share the same backend configuration. You cannot send dev state to one S3 bucket and production state to another.

  3. Same provider versions — all workspaces use the same .terraform.lock.hcl. You cannot test a provider upgrade in dev while keeping production on the old version.

  4. Conditional complexity — as environments diverge, your code fills up with terraform.workspace == "production" ? ... : ... conditionals that are hard to read.

# This gets ugly fast when environments differ significantly
resource "aws_instance" "bastion" {
count = terraform.workspace == "production" ? 1 : 0
ami = var.ami_id
instance_type = "t3.micro"
}

resource "aws_waf_web_acl" "main" {
count = terraform.workspace == "production" ? 1 : 0
# WAF only in production...
}

Alternatives to Workspaces

When workspaces do not fit, consider these alternatives:

Directory-based structure:

environments/
dev/
main.tf
terraform.tfvars
backend.tf
staging/
main.tf
terraform.tfvars
backend.tf
production/
main.tf
terraform.tfvars
backend.tf
modules/
vpc/
app/

Each environment has its own backend.tf with a unique state location, and its own terraform.tfvars with environment-specific values. The shared logic lives in modules.

Terragrunt:

# terragrunt.hcl in environments/dev/
terraform {
source = "../../modules/app"
}

inputs = {
environment = "dev"
instance_type = "t3.micro"
}

remote_state {
backend = "s3"
config = {
bucket = "my-state-${get_env("AWS_ACCOUNT_ID")}"
key = "dev/terraform.tfstate"
region = "us-east-1"
}
}

Terragrunt adds DRY backend configuration, dependency management, and per-environment variable files on top of Terraform.

When to Use Workspaces (and When Not)

Use workspaces when:

  • Environments are nearly identical (same resources, different sizes)
  • You are the only person or a small team managing all environments
  • You want zero code duplication between environments
  • You are using a simple setup with one cloud account

Avoid workspaces when:

  • Environments are in different AWS accounts or Azure subscriptions
  • Different environments need fundamentally different resources
  • You need access control per environment
  • Teams manage different environments independently

Wrapping Up

Workspaces give you environment isolation with zero code duplication. They work best when your environments are structurally identical and only differ in sizing. For more complex setups with separate accounts, different resources per environment, or strict access controls, the directory-based approach (optionally with Terragrunt) gives you more flexibility at the cost of some duplication.

Next, we will dive deep into building a complete AWS VPC with Terraform — step by step, with public subnets, private subnets, NAT gateways, and everything you need for a production-ready network.