Terraform Workspaces
The idea in one sentence
Section titled “The idea in one sentence”A Terraform workspace lets one configuration produce several separate state files switched by name, but every workspace still runs through the exact same backend, the exact same provider configuration, and the exact same .tf code.
One configuration, many state files
Section titled “One configuration, many state files”Instead of copy-pasting a whole directory per environment, workspaces let you keep a single set of .tf files and simply switch which named state file Terraform reads from and writes to:
terraform workspace new devterraform workspace new stagingterraform workspace new prod
terraform workspace list# default# * dev# staging# prod
terraform workspace select stagingterraform applyterraform workspace new <name> creates a workspace and switches to it immediately. terraform workspace select <name> switches the active workspace without creating a new one. terraform workspace list shows every workspace that exists, with an asterisk marking the one currently active. The active workspace is tracked locally in the .terraform directory, and every plan or apply you run only ever touches that workspace’s own state — the dev state and the prod state are two entirely separate files, even though both come from the same main.tf.
Varying behavior with terraform.workspace
Section titled “Varying behavior with terraform.workspace”Terraform exposes the active workspace name as a built-in value, terraform.workspace, which you can interpolate directly into resource names, labels, or a lookup map to change behavior per workspace without writing separate code for each one:
locals { machine_types = { dev = "e2-small" staging = "e2-medium" prod = "e2-standard-4" }}
resource "google_compute_instance" "app" { name = "app-${terraform.workspace}" machine_type = lookup(local.machine_types, terraform.workspace, "e2-small") zone = "us-central1-a"
boot_disk { initialize_params { image = "debian-cloud/debian-12" } }}Switch to the prod workspace and this same resource block names the instance app-prod and sizes it as e2-standard-4. Switch to dev and the identical code produces app-dev sized as e2-small. Nothing about the .tf file itself changes between environments — only the active workspace does.
What workspaces do not isolate
Section titled “What workspaces do not isolate”Here is the part that is easy to miss: the backend block, the provider "google" block, and every other line of .tf code are shared, unchanged, across all workspaces.
terraform { backend "gcs" { bucket = "acme-terraform-state" prefix = "app" }}
provider "google" { project = "acme-shared-project" region = "us-central1"}Terraform automatically namespaces each workspace’s state object inside that same bucket so dev, staging, and prod do not overwrite one another’s state file, but the bucket itself, the project in the provider "google" block, and the credentials Terraform authenticates with are identical no matter which workspace is active. There is no hard boundary here at all — nothing stops someone from being in the dev workspace, believing they are about to change a disposable test instance, when in fact every workspace shares the same acme-shared-project GCP project and the same real prod-adjacent resources it contains. A wrong terraform workspace select followed by an apply is a genuinely easy mistake to make, and workspaces alone give you no way to prevent it.
flowchart TD config["Single main.tf + backend + provider google.project = acme-shared-project"] config -->|terraform workspace select dev| devState["dev state file"] config -->|terraform workspace select staging| stagingState["staging state file"] config -->|terraform workspace select prod| prodState["prod state file"] devState -.->|same backend, same project| shared["acme-shared-project"] stagingState -.->|same backend, same project| shared prodState -.->|same backend, same project| shared