Workspaces
A Terraform CLI workspace lets a single configuration directory hold more than one independent state. Every workspace is the same code applied against its own state file, so you can stand up several parallel copies of the same infrastructure — one per developer, one per feature branch, or a quick staging clone — without duplicating any .tf files. They are cheap, built in, and require zero extra tooling, but they also have real limits that make them the wrong choice for environments that diverge in any meaningful way. This page covers how they work, the commands, and exactly where they fit.
What a workspace actually is
When you run terraform init against a configuration, Terraform creates a workspace called default. Every workspace shares the same code and the same backend, but stores its state under a separate key or path so the resources tracked in one workspace are completely isolated from another.
With a local backend, each non-default workspace gets its own file under terraform.tfstate.d/<name>/terraform.tfstate. With a remote backend such as S3, Terraform inserts the workspace name into the state path automatically — your configured key becomes env:/<workspace>/<key> for any workspace other than default. You do not manage these paths yourself; switching workspaces transparently switches the state Terraform reads and writes.
This is identical in OpenTofu — tofu workspace mirrors the commands below exactly.
The terraform.workspace value
Inside your configuration you can read the active workspace name through the terraform.workspace expression. This is the primary lever for making one config behave slightly differently per workspace, typically for naming and tagging:
resource "aws_s3_bucket" "uploads" {
bucket = "devcraftly-uploads-${terraform.workspace}"
tags = {
Environment = terraform.workspace
ManagedBy = "terraform"
}
}
A common pattern is to drive small per-workspace differences from a locals map keyed by the workspace name, falling back to a default:
locals {
instance_type = {
default = "t3.micro"
staging = "t3.small"
prod = "t3.large"
}
selected_type = lookup(local.instance_type, terraform.workspace, "t3.micro")
}
resource "aws_instance" "api" {
ami = "ami-0c7217cdde317cfec"
instance_type = local.selected_type
tags = {
Name = "api-${terraform.workspace}"
}
}
Tip: Never let
terraform.workspacesilently default toprod. Alookupwith a safe, low-cost fallback means an unrecognized or accidentally created workspace deploys something harmless rather than full production sizing.
Workspace commands
All workspace management happens through the terraform workspace subcommands. They operate on the backend configured by terraform init.
| Command | What it does |
|---|---|
terraform workspace list | Lists all workspaces; the active one is marked with * |
terraform workspace new <name> | Creates a workspace and switches to it |
terraform workspace select <name> | Switches to an existing workspace |
terraform workspace show | Prints the name of the current workspace |
terraform workspace delete <name> | Deletes a workspace (must be empty and not active) |
A typical session:
terraform workspace new staging
terraform workspace list
terraform plan
terraform workspace select default
Output:
Created and switched to workspace "staging"!
You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.
default
* staging
Switched to workspace "default".
Because a brand-new workspace starts with empty state, the first plan after creating one shows every resource as a fresh create — you are building a complete second copy, not reusing the first.
Where workspaces fit — and where they don’t
Workspaces shine when you need short-lived, near-identical clones of one configuration. They are a poor fit the moment environments need to differ structurally.
Good fits:
- Ephemeral test or PR environments that are torn down quickly.
- Per-developer sandboxes sharing one config and one backend.
- Multi-region rollouts of a region-agnostic module where only a variable changes.
Poor fits:
- Staging vs production that diverge. Different account boundaries, IAM, sizing, or feature flags become a tangle of
terraform.workspaceconditionals that obscure what each environment really is. - Strong blast-radius isolation. All workspaces live in the same backend — usually the same bucket and often the same cloud account — so a misconfigured backend or credential can touch every environment. Separate directories or backends give a far harder boundary.
- Different variable files per environment. Workspaces do not load
staging.tfvarsautomatically; you must remember to pass-var-file, and forgetting it applies whatever defaults exist.
Warning: It is dangerously easy to run
applyagainst the wrong workspace. The active workspace is invisible unless you check it. Addterraform workspace showto your CI pipeline, or surface the workspace in your shell prompt, before any production apply.
Best practices
- Use workspaces for ephemeral or identical copies; use separate directories or backends for environments that genuinely differ.
- Drive per-workspace differences through a
localsmap pluslookupwith a safe fallback, not scatteredcount/ifconditionals. - Always confirm the active workspace with
terraform workspace showbeforeplanorapply, especially in automation. - Pair workspaces with explicit
-var-filearguments rather than relying on the workspace name to imply configuration. - Keep production in its own backend (and ideally its own account) instead of a
prodworkspace besidedev, so a backend mistake cannot cascade. - Document the expected workspace names in the README so new contributors do not create stray ones.