Terraform · lesson 13 of 17
Beginner
7- 1 Terraform Fundamentals (IaC)
- 2 Terraform Workflow (init → validate → plan → apply)
- 3 Terraform State (tfstate, remote state, locking)
- 4 Terraform Variables and Outputs
- 5 Providers and Resources (the building blocks of Terraform)
- 6 Terraform Locals: Computed Values Without Repetition
- 7 Terraform Modules (reuse, composition, and clean structure)
Intermediate
7- 8 Terraform count and for_each (repeat resources safely)
- 9 The lifecycle Block: prevent_destroy, create_before_destroy, ignore_changes
- 10 Data Sources and Dependencies (read existing infrastructure)
- 11 sensitive = true: What It Hides, and What It Doesn't
- 12 Remote State Safety (locking, drift, and recovery)
- 13 Workspaces and Environments (dev/stage/prod patterns)
- 14 Terraform CI/CD with GitHub Actions (plan/apply)
Advanced
3Also part of Terraform
4Workspaces 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
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.