Multiple Environments: Workspaces and Directory Layout
Throughout the series, every configuration ran in a single state for a single environment. Reality almost always needs more: dev to experiment, staging to verify, prod to run for real, each with its own state so they don't collide. Part V deals with that. This article compares two ways to organize multiple environments — workspaces and directory layout — and, importantly, knowing when to use each, because choosing wrong can let an apply aimed at dev accidentally destroy prod.
Goal
Understand and be able to use workspaces, understand directory layout, and know clearly the limits of workspaces so you don't use them for something they don't fit.
The problem: one state isn't enough
If dev and prod share one state, they share one list of resources — you can't have two separate "app" buckets. Each environment needs its own state. The two solutions differ in where that separate state lives.
Approach 1: workspaces
Workspaces allow multiple named states within the same configuration and backend. Each workspace holds its own state; when you're in the dev workspace, Terraform only sees dev's resources, while prod's resources remain intact but invisible until you switch over.
A configuration references the current workspace via terraform.workspace to change behavior:
locals {
instance_type = terraform.workspace == "prod" ? "t3.small" : "t3.micro"
}
resource "aws_s3_bucket" "app" {
bucket_prefix = "tf-series-bai15-${terraform.workspace}-"
force_destroy = true
tags = {
Workspace = terraform.workspace
Size = local.instance_type
}
}
The workspace management commands:
$ terraform workspace list
* default
$ terraform workspace new dev
$ terraform workspace new prod
$ terraform workspace list
default
dev
* prod
Applying in each workspace gives different results according to terraform.workspace:
$ terraform workspace select dev && terraform apply -auto-approve
$ terraform output
bucket = "tf-series-bai15-dev-2026..."
instance_type = "t3.micro"
workspace = "dev"
$ terraform workspace select prod && terraform apply -auto-approve
$ terraform output
bucket = "tf-series-bai15-prod-2026..."
instance_type = "t3.small"
workspace = "prod"
Two separate buckets, prod using t3.small and dev t3.micro, from the same file. With the local backend, each workspace's state lives in its own directory:
$ find terraform.tfstate.d -name '*.tfstate'
terraform.tfstate.d/dev/terraform.tfstate
terraform.tfstate.d/prod/terraform.tfstate
(With the S3 backend, each workspace becomes a separate key in the same state bucket.)
An important limit of workspaces
Workspaces are convenient, but there's a warning in the docs that many people overlook, leading to serious incidents. The docs say it plainly: workspaces are "inappropriate for system decomposition or deployments requiring separate credentials and access controls".
The reason lies in the words same backend. Every workspace shares one backend, one set of credentials, one configuration. The practical consequences:
First, you can't separate permissions. Anyone who can apply dev can also apply prod, because they share credentials and share a backend — you can't grant someone dev-only access.
Second, fatal mistakes are easy. Switching workspace is just a select command, and nothing stops you from forgetting you're in prod and running destroy. The current workspace is hidden state in the terminal, not shown in any file.
Third, prod should be in its own AWS account for real isolation (blast radius), whereas workspaces force a shared provider/credential.
Workspaces fit lightweight variants of the same thing: multiple temporary review copies, multiple regions of the same stack. They don't fit a prod–dev boundary that needs strong isolation.
Approach 2: directory layout
The recommended approach for strong separation is to split each environment into its own directory, each with its own backend (separate key, even a separate bucket/account), all calling one shared module:
layout-demo/
modules/app/ # shared logic, written once
environments/
dev/ main.tf # backend key = dev/app.tfstate
prod/ main.tf # backend key = prod/app.tfstate
Each environment directory is thin, declaring only its own backend and calling the module with that environment's parameters:
# environments/prod/main.tf
terraform {
# backend "s3" { bucket = "...", key = "prod/app.tfstate", region = "ap-southeast-1" }
}
provider "aws" { region = "ap-southeast-1" }
module "app" {
source = "../../modules/app"
environment = "prod"
instance_type = "t3.small"
}
workspace directory layout
───────── ────────────────
1 dir, 1 backend environments/dev/ -> backend key dev
├─ state "dev" environments/prod/ -> backend key prod
├─ state "prod" (credentials/account can be separate)
└─ shared credential │
select via `workspace select` └─ both call modules/app
You cd environments/prod before running commands, so the current environment is plainly visible in the path, not hidden like a workspace. A separate backend lets prod use its own state bucket and credentials. CI can restrict who runs which directory. In exchange, there's a bit of repetition of the terraform{}/provider skeleton across directories — the price you pay for clarity and safety.
Which to choose
The short rule: use workspaces for lightweight, temporary variants that don't need permission separation (review apps, multiple regions of the same stack). Use directory layout for real environment boundaries — dev/staging/prod — where you want separate state, separate credentials, and reduced risk of mistaken operations. For most projects with a serious prod, directory layout is the default you should choose.
🧹 Cleanup
$ terraform workspace select prod && terraform destroy -auto-approve
$ terraform workspace select dev && terraform destroy -auto-approve
$ terraform workspace select default
$ terraform workspace delete dev && terraform workspace delete prod
Wrap-up
Workspaces keep multiple states in the same backend, selected via terraform workspace select, changing behavior via terraform.workspace — neat for lightweight variants, but not a fit for strong separation because they share a backend and credentials, and the current environment is hidden, which invites mistakes. Directory layout splits each environment into its own directory with its own backend, calling a shared module; it's clearer, supports permission separation, gives prod real isolation, at the cost of a little repeated configuration skeleton. For a serious dev/staging/prod, choose directory layout.
Splitting into directories raises a new question: how does one directory read another's output (for example, compute needs the VPC id that network created in a different state)? Article 16 answers with terraform_remote_state, and also handles two common refactoring situations when reorganizing code: renaming/moving a resource with the moved block, and removing a resource from state with the removed block — replacing the manual commands from Article 7.