Workspaces & environments
dev, staging, prod from one codebase.
Real systems have multiple environments — dev, staging, production — that should be near-identical but separate, and a core IaC skill is managing them from one codebase without copy-paste. There are two main approaches, and knowing the tradeoff matters. Terraform workspaces let one configuration hold multiple independent states (one per workspace), switching between them; the more common and robust approach is separate directories or state files per environment, each calling the same modules with different inputs. Both give you "one codebase, many environments"; they differ in isolation.
# approach 1: workspaces — one config, separate state per workspace$ terraform workspace new staging$ terraform workspace select prod$ terraform workspace listdefaultstaging* prod# terraform.workspace is available in code, e.g. to size things per env
The directory-per-environment pattern
The pattern most teams prefer for production is a directory per environment, each with its own backend/state and a small root config that calls shared modules with environment-specific inputs. This gives strong isolation — prod and dev have entirely separate state, so a mistake in dev cannot possibly touch prod, and you can grant different access to each. The shared logic lives in modules; the per-environment directories are thin, just wiring the modules with the right variables. It is more files than workspaces but far clearer blast-radius boundaries.
modules/ # shared, reusable logicnetwork/web/environments/dev/main.tf # calls modules with dev inputs; own backend/stateterraform.tfvars # env = "dev", instance sizes, etc.prod/main.tf # same modules, prod inputs; SEPARATE stateterraform.tfvars # env = "prod"
Variables per environment
The differences between environments — instance sizes, replica counts, domain names — live in variables, supplied per environment via .tfvars files (dev.tfvars, prod.tfvars) or CI variables. The code is identical; only the inputs change, so staging genuinely mirrors production and the only differences are the ones you explicitly declared. This is what makes environments trustworthy: when the code is shared and only reviewed variables differ, "it worked in staging" actually means something for production.