Skip to content

Terraform Workspaces

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.

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:

Terminal window
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
terraform workspace list
# dev
# staging
# * prod
terraform workspace select dev

The 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:

Terminal window
tree terraform.tfstate.d
terraform.tfstate.d
├── dev
└── terraform.tfstate
├── staging
└── terraform.tfstate
└── prod
└── terraform.tfstate

With 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.

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.

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
One Terraform config and one backend, three workspaces, three state files, but everything else shared including the Azure subscription
What does creating a new Terraform workspace actually isolate from other workspaces
Which of these is shared identically across every workspace in the same configuration
What is terraform.workspace typically used for
What is the specific real-world risk that Terraform workspaces introduce