Skip to main content
Terraform · lesson 13 of 17
Terraform intermediate Lesson 13 of 17

Workspaces and Environments (dev/stage/prod patterns)

Terraform workspaces give one config separate state per environment. See them work, fail on an unknown workspace, and compare with a directory per env.

By SkillByExample · Updated

terraform workspaces environments

One configuration often has to serve several environments, each with its own state. Terraform offers two ways to do that: workspaces (one directory, several states) and a directory per environment. Examples use Terraform 1.16.5 and hashicorp/random 3.9.1.

One configuration, a state per workspace

terraform {
  required_providers {
    random = {
      source  = "hashicorp/random"
      version = "~> 3.9"
    }
  }
}

locals {
  env = terraform.workspace

  replicas = {
    default = 1
    dev     = 1
    prod    = 3
  }
}

resource "random_pet" "app" {
  count  = local.replicas[local.env]
  prefix = "app-${local.env}"
}

output "names" {
  value = random_pet.app[*].id
}
$ terraform workspace list
* default

$ terraform workspace new prod
Created and switched to workspace "prod"!

You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.
$ terraform apply -auto-approve -no-color | tail -7
Outputs:

names = [
  "app-prod-emerging-marten",
  "app-prod-enhanced-grizzly",
  "app-prod-rational-molly",
]
$ terraform workspace select default
Switched to workspace "default".
$ terraform apply -auto-approve -no-color | tail -5
Outputs:

names = [
  "app-default-optimum-mustang",
]
$ find . -name "*.tfstate"
./terraform.tfstate
./terraform.tfstate.d/prod/terraform.tfstate

The same .tf files produced three pets in prod and one in default. terraform.workspace fed both the count and the names. Each workspace has its own state file: default uses the usual terraform.tfstate, and every other workspace gets a directory under terraform.tfstate.d/. With a remote backend, the workspace name goes into the state key instead.

A workspace the config doesn’t expect

$ terraform workspace new -no-color staging | head -1
Created and switched to workspace "staging"!
$ terraform plan -no-color

Error: Invalid index

  on main.tf line 21, in resource "random_pet" "app":
  21:   count  = local.replicas[local.env]
    ├────────────────
    │ local.env is "staging"
    │ local.replicas is object with 3 attributes

The given key does not identify an element in this collection value.

Anyone can create a workspace with any name, and the config only knows the three it lists. This failure is actually the safe outcome. The risky case is a lookup that silently falls back to a default. Choose deliberately:

  count  = lookup(local.replicas, local.env, 1)
$ terraform plan -no-color | grep -E "^  #|Plan:"
  # random_pet.app[0] will be created
Plan: 1 to add, 0 to change, 0 to destroy.

lookup with a third argument gives any unknown workspace one replica. That’s sensible for short-lived feature environments, but it would also quietly give a mistyped prdo one replica.

Deleting a workspace that still has resources

$ terraform workspace select default
Switched to workspace "default".
$ terraform workspace delete -no-color prod

Error: Workspace is not empty

Workspace "prod" is currently tracking the following resource instances:
  - random_pet.app[0]
  - random_pet.app[1]
  - random_pet.app[2]

Deleting this workspace would cause Terraform to lose track of any associated
remote objects, which would then require you to delete them manually outside
of Terraform. You should destroy these objects with Terraform before deleting
the workspace.

If you want to delete this workspace anyway, and have Terraform forget about
these managed objects, use the -force option to disable this safety check.

$ terraform workspace delete staging
Deleted workspace "staging"!

staging never applied anything, so it was deleted. prod still tracks three resources, and deleting its state would leave them running with nothing managing them. That’s the same “lost state” problem as deleting terraform.tfstate, and Terraform blocks it here too.

Practice

1. Find out which workspace you’re in, from the CLI and from inside the configuration.

Solution
$ terraform workspace show
default
$ echo 'terraform.workspace' | terraform console
"default"

No plan output names the workspace for you. Put it in a resource name or tag, and have CI run terraform workspace show before every apply.

2. Delete prod the right way.

Solution
$ terraform workspace select prod
Switched to workspace "prod".
$ terraform destroy -auto-approve -no-color | tail -1
Destroy complete! Resources: 3 destroyed.
$ terraform workspace select default
Switched to workspace "default".
$ terraform workspace delete prod
Deleted workspace "prod"!

Destroy from inside the workspace, then switch away, because you can’t delete the workspace you’re in.

3. Rebuild the same setup as a directory per environment: a shared modules/app and thin envs/dev and envs/prod root modules, each with its own state.

Solution
# modules/app/main.tf
terraform {
  required_providers {
    random = { source = "hashicorp/random", version = "~> 3.9" }
  }
}

variable "env" { type = string }
variable "replicas" { type = number }

resource "random_pet" "app" {
  count  = var.replicas
  prefix = "app-${var.env}"
}

output "names" { value = random_pet.app[*].id }
# envs/prod/main.tf (envs/dev is the same with "dev" and 1)
module "app" {
  source   = "../../modules/app"
  env      = "prod"
  replicas = 3
}

output "names" { value = module.app.names }

After terraform -chdir=envs/dev init and terraform -chdir=envs/prod init:

$ terraform -chdir=envs/prod apply -auto-approve -no-color | tail -7
Outputs:

names = [
  "app-prod-adapted-lemming",
  "app-prod-fond-bluejay",
  "app-prod-together-herring",
]
$ terraform -chdir=envs/dev plan -no-color | grep Plan:
Plan: 1 to add, 0 to change, 0 to destroy.
$ find envs -name terraform.tfstate
envs/prod/terraform.tfstate

The environment is now in the path you type and in every file name a reviewer sees, and each directory can have its own backend and credentials. The cost is a few duplicated lines per environment. Keep them thin and put all the logic in the module.

Next: Terraform in CI/CD with GitHub Actions.

Frequently Asked Questions

What is a Terraform workspace?
A named, separate state for the same configuration. Every working directory starts in a workspace called default; terraform workspace new prod creates another, and the local backend stores its state under terraform.tfstate.d/prod/. The config reads the current name through terraform.workspace.
Should I use workspaces for dev, staging and prod?
They work for environments that are truly identical apart from sizing, but many teams prefer a directory per environment calling shared modules. Directories make the environment visible in the path and in code review, allow different backends and credentials per environment, and remove the risk of applying while the wrong workspace is selected.
How do I know which workspace I'm in?
terraform workspace show prints it, and terraform workspace list marks it with an asterisk. Nothing in plan output names the workspace except values you derive from terraform.workspace, so include it in resource names or tags and check it in CI before any apply.
Why can't I delete a workspace?
Terraform refuses to delete a workspace whose state still tracks resources, because deleting the state would leave those resources running with nothing managing them. Select the workspace, run terraform destroy, then switch away and delete it; -force skips the check and should be used only when the resources are already gone.
Are Terraform CLI workspaces the same as HCP Terraform workspaces?
No. In HCP Terraform (formerly Terraform Cloud), a workspace is a full unit with its own variables, permissions and run history, usually one per environment. CLI workspaces are just multiple state files for one configuration in one backend.