Chapter 4: State on GCS
Topic 24

Workspaces

State

A workspace is a named, separate state under the same configuration and backend. terraform.workspace evaluates to the current one, and switching workspaces switches which state file you read and write — the config and the bucket stay the same, only the state object changes. They are a real isolation mechanism, and a frequently misused one.

On GCP, workspaces are usually the wrong tool for environments. The project is already the natural boundary for isolation, and a workspace gives you neither IAM nor quota separation — just a separate state under one shared project and one shared provider config. Knowing exactly what a workspace does and does not isolate is what keeps you from reaching for them where a project belongs.

What a Workspace Is

Run terraform workspace new staging and Terraform creates a second state under the same backend. With the gcs backend, each workspace gets its own state object under the prefix — Terraform writes non-default workspaces to a <prefix>/<workspace>.tfstate path — so default and staging never touch each other's resources even though they share one config and one bucket prefix.

Creating and switching workspaces
terraform workspace new sandbox-anna   # create and switch to it
terraform workspace list             # * marks the active one
terraform workspace show             # print the active workspace name
terraform workspace select default   # switch back

The active workspace decides which state file the next plan and apply read and write. There is always a default workspace; the others you create explicitly. The prompt does not show which one is active, which is the root of the most common workspace accident.

Referencing the Current Workspace

terraform.workspace is a string you can interpolate into resource names and labels — "hatch-events-raw-${terraform.workspace}" — so one config produces per-workspace resources. This is what makes a workspace useful: the same HCL stamps out an isolated copy of the infrastructure for each workspace, named so the copies do not collide.

It is also the source of the "I applied to the wrong workspace" outage. Because the resource names depend on the active workspace, and the active workspace is not shown in the prompt, it is easy to run an apply meant for one workspace against another — and watch it create or change the wrong copy. Always confirm with terraform workspace show before an apply.

Where Workspaces Actually Fit

Workspaces fit short-lived parallel copies of the same infrastructure. A per-developer sandbox where each engineer wants their own isolated stack of the Hatch pipeline, an ephemeral PR preview spun up and torn down per pull request, a quick experiment you will delete in an hour — these are cases where you want identical resources with isolated state and zero new project plumbing.

The common thread is sameness and impermanence. Every workspace runs the exact same config, and you do not care that they share a project's IAM and quota because they are throwaway. The moment a copy needs to outlive the day or differ from the others, the fit breaks.

Why a Project per Environment Usually Wins on GCP

For dev, staging, and prod, a project per environment is the better boundary. A hatch-pipeline-dev and a separate hatch-pipeline-prod give you separate billing, separate quota, separate IAM policy, and separate blast radius. Workspaces share one project and one provider config, so a fat-fingered apply can hit prod resources with dev's permissions — there is no boundary in between to stop it.

This is the GCP-native isolation. On Google Cloud the project is the real security and cost boundary, and structuring environments as projects (covered in depth in the project-structure chapter) gives you isolation that a workspace, sharing everything but the state object, simply cannot provide.

What each boundary isolates
Workspace
Isolates state only, under one shared project, provider, and IAM. Fits ephemeral copies.
Project per env
Isolates state plus billing, quota, IAM, and blast radius. Fits dev, staging, prod.

The Sameness Constraint

Workspaces force every environment through the identical config and provider settings. That is fine while the environments truly are identical, and it falls apart the moment prod needs a different project, a different region, or a different CMEK key. To serve divergent environments from one root, you start sprinkling terraform.workspace == "prod" ? ... : ... conditionals through the config.

Those conditionals sprawl. One or two are tolerable; a dozen scattered across resources turn the config into a branching mess where reading any single resource means tracing which workspace it is for. At that point a separate root module per environment reads cleaner and is harder to misfire — the divergence is the signal to split, not to branch.

Workspaces vs Project-per-Environment

Workspaces — isolate state under one config, backend, and project. Right for ephemeral, identical copies: PR previews, per-developer sandboxes, throwaway experiments where a whole project would be overkill.

Project-per-environment — isolates state and billing, quota, IAM, and blast radius. Right for dev, staging, and prod, because on GCP the project is the real security and cost boundary and a workspace gives you none of it.

Common Mistakes
  • Using workspaces for dev/staging/prod and discovering they share one project's IAM and quota — a misfired prod apply runs with the same credentials as dev, with no boundary to stop it.
  • Building a resource and forgetting which workspace is active, since the prompt doesn't show it — you apply staging changes into default; always check terraform workspace show first.
  • Sprawling terraform.workspace == "prod" conditionals through the config to force one root to serve divergent environments — at that point separate roots are simpler and safer.
  • Assuming the default workspace is special or deletable — it always exists and cannot be removed, and naming real environments only as non-default workspaces leaves default as a confusing empty footgun.
  • Treating a per-developer sandbox workspace as permanent — workspaces shine for ephemeral copies, and a long-lived sandbox quietly accumulates the same divergence that breaks the sameness constraint.
Best Practices
  • Reserve workspaces for ephemeral, identical copies — PR previews, per-developer sandboxes — where a whole project would be overkill.
  • Use a project per environment for dev/staging/prod so isolation covers IAM, quota, and billing, not just state.
  • Print terraform.workspace in plan output or your shell prompt so you always know which state you are about to write.
  • When environments must differ in project, region, or keys, split into separate root modules instead of branching on terraform.workspace.
  • Leave the default workspace meaningful rather than naming every real environment as a non-default workspace and stranding default as an empty trap.
Comparable tools AWS s3 backend workspaces the identical mechanism on AWS Terragrunt per-environment directories as the project-per-env analog Pulumi stacks closer to project-per-env than to TF workspaces

Knowledge Check

What does a Terraform workspace isolate, and what does it not?

  • It isolates state under one shared config, backend, and project — but not IAM, quota, or billing
  • It isolates the project, IAM, and quota too, giving each workspace full end-to-end environment separation
  • It isolates the provider binary so each workspace can pin and run a different version
  • It isolates nothing real at all; it is purely a naming convention over one state

Why is a project per environment preferred over workspaces for dev/staging/prod on GCP?

  • The project is GCP's real security and cost boundary, giving separate IAM, quota, billing, and blast radius that a workspace cannot
  • Workspaces are formally deprecated and slated for complete removal in an upcoming Terraform major release, so configs that still rely on them will eventually stop working
  • Separate projects are meaningfully cheaper to run than the equivalent set of workspaces
  • Workspaces cannot be used together with the gcs backend at all, only with local state

Where does the gcs backend store the state for a non-default workspace?

  • In a separate <prefix>/<workspace>.tfstate object under the same prefix
  • In a completely separate bucket that the backend creates fresh for each workspace
  • In a DynamoDB-style lock table keyed by the workspace name and config prefix
  • In the same shared default.tfstate object, namespaced internally by workspace

Which situation is the right fit for a workspace rather than a separate project?

  • An ephemeral PR preview that is an identical copy of the infrastructure and gets torn down after the pull request
  • A long-lived production environment that needs its own tightly scoped IAM policy, dedicated quota limits, and an isolated blast radius from the other environments
  • A staging environment that runs in a different region with a different CMEK key
  • Any environment where billing must be cleanly separated and tracked on its own invoice

You got correct