Managing environment-specific variables with Terraform workspaces
Terraform workspaces provide a practical way to keep separate state files for development, testing and production while using the same configuration. For a small infrastructure team, this can reduce duplicated code and make environment promotion easier to control.
The useful distinction is between configuration and state. A workspace selects the state Terraform reads and writes; it does not automatically change instance sizes, subnets, regions or credentials. Those values must be supplied through variables, locals, workspace settings or separate configuration files.
Understand what a workspace isolates
A local Terraform workspace creates a separate state snapshot under the same working directory. The default workspace is named default, while additional workspaces can be created with commands such as:
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
terraform workspace select staging
terraform workspace show
When the configuration uses terraform.workspace, it can choose values appropriate to the active environment. This is useful for naming resources, selecting CIDR ranges and applying different capacity settings without copying an entire directory tree.
Workspaces are most suitable when environments have broadly similar architecture and lifecycle requirements. A Sydney-based team might use the same AWS design in ap-southeast-2 for development and production, while changing instance counts or database sizes. For substantially different accounts, regions or compliance boundaries, separate root modules and backend configurations are often safer than relying on workspace selection alone.
Define a clear variable model
A map of objects is a reliable pattern for environment-specific values. It keeps related settings together and makes missing values easier to detect than a collection of unrelated conditional expressions.
variable "environment_settings" {
type = map(object({
instance_type = string
instance_count = number
vpc_cidr = string
}))
default = {
dev = {
instance_type = "t3.small"
instance_count = 1
vpc_cidr = "10.10.0.0/16"
}
staging = {
instance_type = "t3.medium"
instance_count = 2
vpc_cidr = "10.20.0.0/16"
}
prod = {
instance_type = "m6i.large"
instance_count = 3
vpc_cidr = "10.30.0.0/16"
}
}
}
locals {
settings = var.environment_settings[terraform.workspace]
}
Resources can then refer to local.settings.instance_type and local.settings.vpc_cidr. A validation rule or a precondition should ensure that only approved workspace names are used. This avoids an accidental terraform workspace new production creating a new state file with no matching settings.
For sensitive data, avoid placing passwords or long-lived access keys in variable files. Terraform state can contain rendered resource arguments, so use AWS Secrets Manager, SSM Parameter Store or an external secrets workflow. Workspace variables in HCP Terraform can be marked sensitive, but sensitive marking controls display rather than removing values from state.
Use workspace variable files consistently
Terraform automatically loads files named terraform.tfvars and files ending in .auto.tfvars. Environment-specific files such as dev.tfvars are not loaded automatically, so the selected workspace should be paired with an explicit command:
terraform plan \
-var-file="env/${TF_WORKSPACE}.tfvars"
A wrapper script or CI job can set TF_WORKSPACE, select the workspace and pass the matching file. This creates a repeatable workflow for engineers working from Brisbane, Melbourne or Perth, where local shell habits and time zones should not alter the target environment.
In HCP Terraform, workspace variables and variable sets can replace local .tfvars files. A shared variable set might provide an AWS region or tagging convention, while an individual workspace supplies its instance size and network ranges. Keep the naming convention identical across local development and CI so that dev, staging and prod always resolve to the same intended values.
Control state, regions and naming
A workspace should be selected before planning, and the selected name should be visible in CI logs. A common pipeline sequence is:
terraform init
terraform workspace select "$TF_WORKSPACE" ||
terraform workspace new "$TF_WORKSPACE"
terraform plan -out=tfplan
terraform apply tfplan
In production, automatic workspace creation deserves caution. A typo in a pipeline variable can create a new empty state and produce a plan that attempts to build unexpected resources. An allow-list such as dev staging prod, combined with approval gates for production, provides a simple safeguard.
Resource names and tags should include the workspace where practical:
locals {
name_prefix = "orders-${terraform.workspace}"
common_tags = {
Environment = terraform.workspace
ManagedBy = "Terraform"
Owner = "platform"
}
}
For Australian workloads, explicitly define the AWS region rather than assuming the operator’s default profile. ap-southeast-2 is the usual Sydney region, while ap-southeast-4 serves Melbourne. Teams with users in Western Australia should test latency before choosing a primary region. Network design also matters: guidance on VPC internet access can help validate route tables, internet gateways and subnet behaviour before applying workspace-specific CIDR ranges.
Avoid common workspace mistakes
Workspaces do not provide complete security isolation. Anyone with access to the backend may be able to access state for multiple environments, and a shared set of credentials can still modify production. Use separate AWS accounts for stronger boundaries, with distinct IAM roles and narrowly scoped CI permissions. This is particularly relevant where Australian organisations need clear separation for customer data, audit evidence or internal change controls.
Another common mistake is embedding environment logic throughout resource blocks:
count = terraform.workspace == "prod" ? 3 : 1
A few simple conditions are acceptable, but repeated expressions become difficult to review. Put decisions in locals, validate inputs, and make plans easy to compare. Keep state locking enabled, use remote state for team workflows, and never commit generated state or secret-bearing variable files to Git.
A workspace-based design works best when its contract is documented: approved names, variable sources, account and region mapping, backend location, and the approval process for production. For engineers using IDEs or experimenting in a home lab, a separate disposable workspace can be useful; related developer tooling references, such as the NetBeans blog, can support local workflow experimentation without mixing it with managed infrastructure state.