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 apply in 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 cloud block, terraform workspace maps onto those instead.

Workspace Commands

CommandNotes
terraform workspace list* marks the current one
terraform workspace list -jsonmachine-readable (1.16+)
terraform workspace showprints just the current name
terraform workspace new NAMEcreates and switches, optional -state=FILE to seed
terraform workspace select NAMEswitches, -or-create creates it if missing
terraform workspace delete NAMEmust not be current, refuses if state is non-empty
terraform workspace delete -force NAMEdeletes 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 dev can apply to prod, 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 apply gives you no reminder of which one you are in, and -var-file does 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.tfvars

Separate 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 destroy before workspace delete. Deleting a workspace with resources in it needs -force and 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 init first, and never assume the previously selected workspace carried over. A fresh runner starts in default.