Workspaces
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.
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.
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 — 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.
- 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 checkterraform workspace showfirst. - 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
defaultworkspace is special or deletable — it always exists and cannot be removed, and naming real environments only as non-default workspaces leavesdefaultas 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.
- 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.workspacein 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
defaultworkspace meaningful rather than naming every real environment as a non-default workspace and strandingdefaultas an empty trap.
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
gcsbackend at all, only with local state
Where does the gcs backend store the state for a non-default workspace?
- In a separate
<prefix>/<workspace>.tfstateobject 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.tfstateobject, 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