Terraform Cheatsheet
Workspaces
Use this Terraform reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
What Workspaces Are
A CLI workspace is a named state file behind one backend configuration. The configuration, the providers, the backend and the credentials are all shared. Only the state differs.
Every configuration starts with one workspace called default, and you cannot delete it.
terraform workspace show # default terraform workspace new dev # creates AND switches, state is empty terraform apply # applies into dev's state
Two things people assume that are not true:
- A workspace is not an isolation boundary. Same backend bucket, same credentials, same provider configuration, same code. A bad
applyin the wrong workspace reaches production because there is nothing between you and it. - A CLI workspace is not the same thing as an HCP Terraform workspace. HCP workspaces are separate objects with their own variables, credentials and run history. When you use a
cloudblock,terraform workspacemaps onto those instead.
Workspace Commands
| Command | Notes |
|---|---|
terraform workspace list | * marks the current one |
terraform workspace list -json | machine-readable (1.16+) |
terraform workspace show | prints just the current name |
terraform workspace new NAME | creates and switches, optional -state=FILE to seed |
terraform workspace select NAME | switches, -or-create creates it if missing |
terraform workspace delete NAME | must not be current, refuses if state is non-empty |
terraform workspace delete -force NAME | deletes anyway, orphaning every resource in it |
terraform workspace select prod || terraform workspace new prod terraform workspace select -or-create=true prod # same thing, one call terraform workspace list -json | jq -r '.[]'
workspace new and workspace select require an initialized directory, so terraform init comes first. The selected workspace is stored locally in .terraform/environment, which means it is per checkout, not per branch. Two terminals in the same directory share it.
terraform.workspace in Configuration
locals { env = terraform.workspace # "default", "dev", "prod", ... } resource "aws_instance" "web" { instance_type = terraform.workspace == "prod" ? "m6i.large" : "t3.micro" tags = { Environment = terraform.workspace } } # Cost control: only build the expensive thing in prod resource "aws_nat_gateway" "main" { count = terraform.workspace == "prod" ? 1 : 0 allocation_id = aws_eip.nat[0].id subnet_id = aws_subnet.public[0].id }
terraform.workspace cannot be used in a backend block, in required_providers, or anywhere else that is evaluated before the workspace is known. It also cannot be used in a module source.
Name every shared resource with the workspace in it, or the second workspace fails on a name collision.
name = "${var.project}-${terraform.workspace}-assets"Per-Workspace Configuration
A lookup map keyed by workspace name keeps the branching in one place.
locals { sizing = { default = { instance_type = "t3.micro", min = 1, max = 2 } dev = { instance_type = "t3.micro", min = 1, max = 2 } prod = { instance_type = "m6i.large", min = 2, max = 10 } } # Fails loudly on an unknown workspace instead of silently defaulting cfg = local.sizing[terraform.workspace] } resource "aws_autoscaling_group" "app" { min_size = local.cfg.min max_size = local.cfg.max }
Per-workspace variable files are a shell-level concern. terraform.workspace is an HCL reference and means nothing to bash.
# Correct: read the name out of the CLI first WS="$(terraform workspace show)" terraform plan -out=tfplan -var-file="envs/${WS}.tfvars" # Or set it from the pipeline, which is clearer in CI terraform workspace select -or-create=true "$ENVIRONMENT" terraform plan -out=tfplan -var-file="envs/${ENVIRONMENT}.tfvars"
There is no automatic per-workspace tfvars loading. A dev.tfvars file is not picked up because you are in the dev workspace, and forgetting -var-file applies the defaults instead of failing.
State Layout
s3://acme-tf-state/ ├── prod/terraform.tfstate # the "default" workspace, your `key` └── env:/ # workspace_key_prefix, default "env:" ├── dev/prod/terraform.tfstate └── staging/prod/terraform.tfstate
The default workspace uses key verbatim, and every other workspace is stored at <workspace_key_prefix>/<name>/<key>. Only backends that support multiple states allow workspaces at all: s3, gcs, azurerm, consul, kubernetes, pg, oss, cos, local and the remote/cloud integrations. The http backend does not.
When Workspaces Are the Wrong Tool
For separating production from everything else, workspaces are usually the wrong tool. Use a directory per environment with its own backend configuration.
Why:
- One backend, one bucket, one set of credentials. Whoever can apply to
devcan apply toprod, and there is no per-environment access control to grant. - One code path. A change intended for dev ships to prod the moment someone selects that workspace. There is no way to promote a change through environments, because there is only one version of the code.
- The selected workspace is invisible ambient state.
terraform applygives you no reminder of which one you are in, and-var-filedoes not protect you. - Config drifts into
terraform.workspace == "prod" ? ... : ...conditionals scattered across files, which is a per-environment codebase with none of the safety. - Environments never stay identical. The first prod-only replica or prod-only WAF turns every module into a branch.
live/
├── modules/ # the shared, versioned modules
├── dev/
│ ├── main.tf # calls modules/, thin
│ ├── backend.tf # key = "dev/terraform.tfstate"
│ └── terraform.tfvars
└── prod/
├── main.tf
├── backend.tf # DIFFERENT bucket, DIFFERENT role
└── terraform.tfvarsSeparate directories give you separate backends, separate credentials, separate plan output, and a real promotion path (bump the module version in prod/ when dev/ looks good). The cost is a small amount of near-duplicate root config, which is cheap next to a wrong-workspace apply.
Workspaces earn their place for:
- Short-lived per-branch or per-pull-request environments that are created, tested and destroyed.
- A single developer running a scratch copy of a stack.
- Genuinely identical regional or per-tenant copies where the only difference is a name.
CI and Ephemeral Environments
# Per-pull-request environment, torn down on close WS="pr-${PR_NUMBER}" terraform init -input=false terraform workspace select -or-create=true "$WS" terraform plan -input=false -out=tfplan -var="image_tag=$GIT_SHA" terraform apply -input=false tfplan
# Teardown. Order matters: destroy, leave, then delete. terraform workspace select "pr-${PR_NUMBER}" terraform destroy -auto-approve -input=false terraform workspace select default terraform workspace delete "pr-${PR_NUMBER}"
terraform destroybeforeworkspace delete. Deleting a workspace with resources in it needs-forceand leaves those resources running with no state, which is a cloud bill nobody can trace.- Workspace names appear in resource names and in the state path, so keep them short and DNS-safe. Stick to letters, digits,
-and_. - In CI, always
initfirst, and never assume the previously selected workspace carried over. A fresh runner starts indefault.