Terraform Modules and Workspaces¶
Modules: reusable, parameterized infrastructure¶
# modules/web-app/main.tf
variable "instance_count" {}
variable "instance_type" {}
resource "aws_instance" "web" {
count = var.instance_count
instance_type = var.instance_type
}
output "instance_ids" {
value = aws_instance.web[*].id
}
# root main.tf
module "staging_app" {
source = "./modules/web-app"
instance_count = 2
instance_type = "t3.micro"
}
module "prod_app" {
source = "./modules/web-app"
instance_count = 10
instance_type = "t3.large"
}
A module is a directory of .tf files with declared input
variables and outputs — called from a root (or another) module
like a function call, with different arguments producing differently
configured infrastructure from the same underlying definition.
This is the direct way to avoid copy-pasting near-identical resource
blocks for staging vs. production — the module defines the shape
once, and each caller supplies only what differs.
Workspaces: same config, isolated state — with a real caveat¶
terraform workspace new staging
terraform workspace select staging
terraform apply # applies against staging's OWN state file
A workspace gives the same HCL configuration its own, separate state
file per workspace — switching workspaces switches which state
plan/apply compares against, without touching the .tf files at
all. This sounds like clean environment separation, but the real
gotcha is operational: which workspace is currently selected is not
visually obvious in a typical terminal — running terraform apply
without checking terraform workspace show first can apply
changes intended for staging against whatever workspace happens to be
currently selected, including production.
Because of that risk, many teams prefer separate directories per
environment (environments/dev/, environments/staging/,
environments/prod/, each with its own explicit backend
configuration and its own .tf files, possibly calling shared
modules) over workspaces for environment separation specifically —
being in the wrong directory is far more visible (the shell prompt,
the files present) than being in the wrong workspace, which requires
remembering to check an easily-forgotten piece of implicit state.
Workspaces remain genuinely useful for other cases — short-lived
per-feature-branch preview environments, where the lower ceremony of
switching workspaces outweighs the wrong-environment risk, because the
consequence of a mistake is smaller.
Variable precedence¶
From lowest to highest priority (a later source overrides an earlier one for the same variable):
The command-line -var flag always wins over any file-based value —
useful for a one-off override, but also a common source of "why isn't
my tfvars file taking effect" confusion when a CI pipeline or a
personal shell alias is quietly passing a -var flag that overrides
what looks, from the tfvars file alone, like it should be in effect.
Common pitfall¶
Running terraform apply after switching to a different task without
re-checking terraform workspace show (or, for the directory-based
approach, without re-checking which directory the shell is actually
in) is the single most common way a change intended for one
environment lands in another — the command itself gives no
environment-specific warning, it just applies against whatever context
is currently active. Making the current environment visible at a
glance (a shell prompt that displays the active workspace, or simply
defaulting to the safer directory-per-environment structure) closes
this gap far more reliably than trusting everyone to remember to check
manually every time.