Skip to content
ExponaBLOG
TERRAFORM, IAC & CLOUD AUTOMATION · DAY 016 / 100

Terraform workspaces vs directories: a step-by-step guide

Two ways to manage environments.

Expona Admin10 min read

Introduction

When you move from a single Terraform configuration to managing multiple environments—dev, staging, production—you'll face a critical decision: do you use Terraform workspaces or organize your code into separate directories? Both approaches work, but they carry different trade-offs in isolation, maintainability, and operational safety. This guide walks you through the decision-making process and shows you practical patterns for each approach so your team can choose the method that fits your infrastructure needs.

Comparing the approaches: workspaces vs directories vs Terragrunt

Before diving into code, let's establish how these three strategies differ and what trade-offs each one makes.

Approach Isolation Code Duplication Operational Safety Team Overhead
Workspaces Separate state files only Low—single config Medium—easy to target wrong workspace Low—minimal setup
Directories Separate state, config, and credentials Medium-High—duplicated blocks High—explicit separation Medium—more files to manage
Terragrunt Separate state and config with DRY pattern Low—shared modules and DRY config High—structured, composable High—learning curve, extra tool

Workspaces let you manage multiple state files from a single configuration and variable set. You switch between workspaces and Terraform applies changes to that workspace's state. The appeal is simplicity—one set of code, minimal boilerplate. The risk is that it's easy to accidentally apply production changes while targeting the wrong workspace, and shared variables can bleed across environments.

Directories give you a separate folder for each environment, each with its own configuration, variables, state, and backend settings. This is more explicit and harder to get wrong by accident, but you'll duplicate resource definitions and maintain multiple configurations. Most teams find this pattern is the safest for production infrastructure.

Terragrunt is a wrapper around Terraform that removes code duplication while preserving isolation. It's powerful but adds operational complexity and requires your team to learn another tool. We'll focus on workspaces and directories in this guide, as they are the most common starting points.

Workspaces: when they fit and when they don't

Terraform workspaces are useful when you need quick, lightweight environment separation. They maintain separate state files but share the same configuration and variable definitions. This works well for short-lived or similar environments where the infrastructure is largely identical.

A workspace example: feature preview environments

Imagine your team deploys temporary preview environments for feature testing. Each preview needs an isolated database and API instance, but the configuration is the same for all of them. Workspaces are a natural fit:

hcl
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

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

provider "aws" {
  region = var.aws_region
}

resource "aws_db_instance" "preview" {
  identifier     = "preview-db-${terraform.workspace}"
  engine         = "postgres"
  instance_class = "db.t3.micro"
  allocated_storage = 20
  username       = var.db_username
  password       = var.db_password
  skip_final_snapshot = true

  tags = {
    Environment = terraform.workspace
    Team        = "platform"
  }
}

resource "aws_ecs_service" "preview_api" {
  name            = "preview-api-${terraform.workspace}"
  cluster         = var.cluster_arn
  task_definition = var.task_definition_arn
  desired_count   = 1

  tags = {
    Environment = terraform.workspace
  }
}

To create and switch workspaces:

hcl
terraform workspace new preview-feature-x
terraform workspace select preview-feature-x
terraform apply -var-file="preview.tfvars"

# Later, clean up
terraform workspace select default
terraform workspace delete preview-feature-x

The workspace name becomes available as terraform.workspace, so you can append it to resource names and tags to keep each environment's resources separate in AWS. This works because the preview environments are temporary and nearly identical in configuration.

Where workspaces break down

Workspaces fall apart when environments need different configurations, different providers, or different credentials. If production needs a multi-AZ RDS instance with automated backups and dev only needs a single-AZ instance with skip-final-snapshot, you'll end up with conditional logic scattered throughout your configuration. Worse, if prod and dev use different AWS accounts, you can't use the same provider block for both—the workspace can't help you there. And if a developer mistakenly runs terraform destroy while on the production workspace, all your prod resources are gone. The shared configuration and state backend create risk.

Directories: isolation by design

The directory-per-environment approach creates a separate folder for each environment, each with its own Terraform configuration, backend settings, and variable definitions. This is more verbose but makes accidents far less likely and gives you explicit control over credentials and provider settings.

Directory structure for production safety

Here's a practical layout:

hcl
infrastructure/
├── dev/
│   ├── main.tf
│   ├── variables.tf
│   ├── terraform.tfvars
│   └── backend.tf
├── staging/
│   ├── main.tf
│   ├── variables.tf
│   ├── terraform.tfvars
│   └── backend.tf
├── prod/
│   ├── main.tf
│   ├── variables.tf
│   ├── terraform.tfvars
│   └── backend.tf
└── modules/
    ├── vpc/
    ├── database/
    └── api_service/

Each environment directory contains its own backend configuration, so dev, staging, and prod use separate state buckets or keys. This prevents cross-environment accidents at the infrastructure level.

Backend isolation example

In prod/backend.tf:

hcl
terraform {
  backend "s3" {
    bucket         = "my-terraform-state-prod"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks-prod"
    encrypt        = true
  }
}

In dev/backend.tf:

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

Notice that prod uses a different bucket entirely and enables encryption. If a developer runs terraform init in the prod directory, they automatically initialize against the prod backend. There's no workspace switching where someone can accidentally target the wrong state.

Shared modules, environment-specific variables

To avoid duplication, keep the core resource definitions in shared modules and parametrize them via variables:

hcl
modules/api_service/main.tf:

resource "aws_ecs_service" "api" {
  name            = var.service_name
  cluster         = var.cluster_arn
  task_definition = var.task_definition_arn
  desired_count   = var.desired_count
  launch_type     = var.launch_type

  tags = {
    Environment = var.environment
  }
}

Then in prod/main.tf:

hcl
module "api_service" {
  source = "../modules/api_service"

  service_name         = "api-prod"
  cluster_arn          = aws_ecs_cluster.prod.arn
  task_definition_arn  = aws_ecs_task_definition.prod.arn
  desired_count        = 3
  launch_type          = "FARGATE"
  environment          = "prod"
}

And in dev/main.tf:

hcl
module "api_service" {
  source = "../modules/api_service"

  service_name         = "api-dev"
  cluster_arn          = aws_ecs_cluster.dev.arn
  task_definition_arn  = aws_ecs_task_definition.dev.arn
  desired_count        = 1
  launch_type          = "FARGATE"
  environment          = "dev"
}

This way, prod uses 3 instances and dev uses 1, but you're not duplicating the module definition. The directory structure enforces that dev and prod changes are applied to their respective backends.

Production isolation: a hard requirement

Production infrastructure demands strict isolation. If you use workspaces for production and staging on the same backend, a single typo in a CI/CD pipeline can destroy the wrong environment. If you use separate directories with separate AWS accounts or role credentials, the blast radius is contained.

Using separate AWS accounts

Many teams run prod in one AWS account and dev/staging in another. With directories, this is straightforward:

hcl
prod/main.tf:

provider "aws" {
  region = var.aws_region

  assume_role {
    role_arn = "arn:aws:iam::PROD_ACCOUNT_ID:role/TerraformRole"
  }
}

dev/main.tf:

provider "aws" {
  region = var.aws_region

  assume_role {
    role_arn = "arn:aws:iam::DEV_ACCOUNT_ID:role/TerraformRole"
  }
}

When a developer runs terraform apply in the prod directory, they assume the prod role. When they run it in dev, they assume the dev role. The credentials are environment-specific, not configuration-specific. Workspaces don't help here—you'd still share the same provider block and have to inject different credentials via environment variables, which is error-prone.

Separate state backends with audit logging

For production, use a dedicated S3 bucket with versioning and access logging enabled:

hcl
resource "aws_s3_bucket" "terraform_state_prod" {
  bucket = "my-org-terraform-state-prod"
}

resource "aws_s3_bucket_versioning" "terraform_state_prod" {
  bucket = aws_s3_bucket.terraform_state_prod.id

  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_logging" "terraform_state_prod" {
  bucket = aws_s3_bucket.terraform_state_prod.id

  target_bucket = aws_s3_bucket.terraform_logs.id
  target_prefix = "prod-state/"
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state_prod" {
  bucket = aws_s3_bucket.terraform_state_prod.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

This setup ensures that every state change is logged and versioned, and you can audit who accessed production state and when. Workspaces on a shared backend can't provide this level of isolation.

Workspaces fit: short-lived environments and testing

Workspaces shine in scenarios where you need temporary, identical environments and safety is less critical:

  • Feature preview environments: Each developer branch gets a workspace with its own database and API instance. Once the PR is merged, delete the workspace.
  • Load testing: Spin up a workspace identical to staging, run your load tests, then tear it down.
  • Hotfix validation: Create a workspace from prod-like configuration, apply the hotfix, validate it, then destroy.

In all these cases, environments are nearly identical, temporary, and the cost of an accident is low. Workspaces let you do this with minimal code and zero infrastructure overhead.

Directory approach fit: multi-env production systems

Use directories when:

  • Environments have significantly different configurations (instance sizes, counts, features).
  • Environments use different AWS accounts or credentials.
  • You need strict audit trails and state isolation.
  • You want to prevent accidental cross-environment changes.
  • You're managing production infrastructure where downtime is costly.

Most production systems fall into this category. The directory approach requires more files but eliminates entire classes of mistakes.

Common mistakes

Mistake 1: Using workspaces for prod and staging on the same backend

This couples your most critical infrastructure with non-critical infrastructure via a single state backend. If a CI/CD script targets the wrong workspace, production goes down. Always use separate directories for prod and staging at minimum.

Mistake 2: Sharing provider blocks across environments

If your dev and prod backends are separate directories but your provider block uses environment variables to swap credentials, you're still at risk. A developer might forget to set the AWS_PROFILE variable before running apply. With separate directories, each with its own provider block pointing to a specific role, there's no ambiguity.

Mistake 3: Mixing workspaces and directories

Some teams use directories for dev and prod but workspaces within the prod directory for "sub-environments." This is confusing and makes it hard for new team members to understand the isolation model. Pick one approach and stick with it across your organization.

Mistake 4: Over-engineering with Terragrunt too early

Terragrunt is powerful, but if your team is still learning Terraform, adding an abstraction layer on top creates cognitive overhead. Start with directories, and if code duplication becomes unmanageable, migrate to Terragrunt later. Most teams find directories are sufficient for years.

Mistake 5: Not versioning or locking state backends

Whether you use workspaces or directories, enable S3 versioning and DynamoDB locks on all state backends. This prevents accidental state corruption and lets you recover from mistakes.

Be consistent: one approach across the organization

The worst scenario is when different teams in your organization use different approaches. A feature team uses workspaces for their service, the platform team uses directories for infrastructure, and the data team uses both. This creates confusion during knowledge transfer, makes it hard to write organization-wide tooling, and increases the chance of mistakes.

Have a conversation with your team early about which approach you'll standardize on. Document the decision in your runbooks and onboarding materials. If you have mixed workloads, you might use workspaces for non-production temporary environments and directories for anything touching production state.

Example organizational standard:

  • Use directories for prod, staging, and any shared infrastructure (networking, databases, IAM).
  • Use workspaces for ephemeral developer environments and feature previews that will be cleaned up in days.
  • All backends are in separate AWS accounts when possible; minimum is separate S3 buckets.
  • All backends have versioning enabled and DynamoDB locks configured.
  • Code review is required for any changes to prod and staging backends.

Write this down. Share it with every engineer who touches infrastructure. As your team and systems grow, consistency pays dividends.

Key takeaways

  • Workspaces are lightweight and suitable for short-lived, nearly-identical environments. They share configuration and backend settings, which makes them simple but risky for production.
  • Directories provide explicit isolation of state, credentials, and configuration. They require more files but are safer and clearer for production infrastructure.
  • Production infrastructure demands separate backends and credentials. Use directories with separate AWS accounts or at minimum separate S3 buckets and DynamoDB tables.
  • Use workspaces for feature previews, load testing, and temporary developer environments. Clean them up when they're no longer needed.
  • Use directories for any environment used by multiple team members or that holds long-lived infrastructure state.
  • Standardize your approach across your organization. Mixed strategies create confusion and increase operational risk.
  • Enable versioning and locking on all state backends regardless of your approach. This prevents corruption and enables recovery.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles