Terraform Workspaces
The idea in one sentence
Section titled “The idea in one sentence”A Terraform workspace lets one configuration keep several separate state files, each under its own name, which is a cheap way to model dev, staging, and prod, but a workspace changes nothing about the backend, the provider, or the Azure subscription that configuration actually talks to.
One configuration, many state files
Section titled “One configuration, many state files”terraform workspace is a built-in command family for managing named state inside a single working directory. You create one workspace per environment, and Terraform keeps a completely separate state file for each:
terraform workspace new devterraform workspace new stagingterraform workspace new prod
terraform workspace list# dev# staging# * prod
terraform workspace select devThe active workspace is just a pointer: whichever one is selected determines which state file the next plan or apply reads and writes. With the default local backend, Terraform stores this under terraform.tfstate.d, one subdirectory per workspace:
tree terraform.tfstate.dterraform.tfstate.d├── dev│ └── terraform.tfstate├── staging│ └── terraform.tfstate└── prod └── terraform.tfstateWith a remote backend such as azurerm, the same idea applies — Terraform namespaces the state blob per workspace instead of per local directory — but the effect is identical: dev cannot see or overwrite prod’s resources, because they live in entirely separate state.
Varying behavior with terraform.workspace
Section titled “Varying behavior with terraform.workspace”Terraform exposes the name of the currently selected workspace as a built-in value, terraform.workspace, which you can interpolate anywhere a string is expected. The most common use is picking a size or a name per environment out of a lookup map:
locals { vm_size = { dev = "Standard_B1s" staging = "Standard_B2s" prod = "Standard_D2s_v5" }}
resource "azurerm_linux_virtual_machine" "app" { name = "app-${terraform.workspace}" resource_group_name = azurerm_resource_group.app.name location = azurerm_resource_group.app.location size = local.vm_size[terraform.workspace]
admin_username = "azureuser" # ...}Select dev and the VM is named app-dev and sized Standard_B1s. Select prod and the same .tf file produces app-prod sized Standard_D2s_v5, with no code duplicated anywhere. This is the appeal of workspaces: one set of .tf files, one place to fix a bug, and per-environment variation driven entirely by which workspace happens to be active.
What a workspace does not isolate
Section titled “What a workspace does not isolate”Here is the part that is easy to miss. terraform workspace new only ever creates a new state file. It does not create a new backend configuration, a new provider configuration, or a new Azure subscription. All of that lives in the same .tf code, shared verbatim across every workspace:
terraform { backend "azurerm" { resource_group_name = "tfstate-rg" storage_account_name = "tfstateacct001" container_name = "tfstate" key = "app.terraform.tfstate" }}
provider "azurerm" { features {} subscription_id = "00000000-0000-0000-0000-000000000000"}Every workspace — dev, staging, and prod alike — reads and writes through that one backend block and authenticates through that one provider block, against that one subscription_id. There is no workspace-scoped credential, no workspace-scoped subscription, and no workspace-scoped copy of the .tf code. The only thing that differs between workspaces is which state file gets touched and whatever you chose to key off terraform.workspace.
That makes workspaces a poor fit for environments that need genuinely separate Azure subscriptions, regions, or credentials, and it creates a specific, real risk: nothing in the tooling stops someone from being on the prod workspace while believing they are still on dev, and applying a change sized and scoped for a toy environment directly against production, because the backend, the provider, and the subscription behind the scenes never changed at all.
flowchart LR config["One Terraform config, one backend block"] -->|terraform workspace new dev| devWs["dev workspace"] config -->|terraform workspace new staging| stagingWs["staging workspace"] config -->|terraform workspace new prod| prodWs["prod workspace"] devWs --> devState["dev state file"] stagingWs --> stagingState["staging state file"] prodWs --> prodState["prod state file"] devWs -.-> shared["Same subscription_id, same backend, same .tf code"] stagingWs -.-> shared prodWs -.-> shared