Per-Environment Variables
Every environment runs the same code but with different data: a smaller instance in dev, a different VPC CIDR in staging, a separate account ID in prod. The job of a variable strategy is to make those differences explicit, reviewable, and impossible to mix up — so you never accidentally apply staging’s database size to production. This page covers per-environment tfvars files, naming conventions that keep them straight, handling secrets per environment, and validating that required values are actually present before Terraform touches your cloud. Everything here applies identically to OpenTofu.
Separate values from code with tfvars files
The cleanest way to vary an environment is to keep the .tf files identical and supply each environment’s values through its own variable-definitions file. Declare your inputs once, then ship a *.tfvars file per environment.
# variables.tf — declared once, shared by every environment
variable "environment" {
type = string
description = "Environment name used for naming and tagging."
}
variable "instance_type" {
type = string
description = "EC2 instance type for the API tier."
}
variable "min_capacity" {
type = number
description = "Minimum number of nodes in the autoscaling group."
}
Each environment gets a file containing only data — no resource blocks, no logic:
# dev.tfvars
environment = "dev"
instance_type = "t3.small"
min_capacity = 1
# prod.tfvars
environment = "prod"
instance_type = "m6i.large"
min_capacity = 3
You select the environment at apply time with -var-file:
terraform apply -var-file="prod.tfvars"
Output:
Terraform will perform the following actions:
# aws_instance.api will be created
+ resource "aws_instance" "api" {
+ instance_type = "m6i.large"
+ tags = {
+ "Environment" = "prod"
}
}
Plan: 1 to add, 0 to change, 0 to destroy.
Tip: Terraform automatically loads
terraform.tfvarsand any*.auto.tfvarsfile without a flag. That convenience is dangerous for multi-environment work — auto-loading the wrong file is exactly the mistake you want to prevent. Name filesdev.tfvars/prod.tfvars(not.auto.tfvars) so the environment is always chosen explicitly on the command line.
Naming conventions that prevent mistakes
A consistent layout makes the “which environment am I touching?” question answerable at a glance. Two common patterns:
| Pattern | Layout | Selection |
|---|---|---|
| Flat tfvars files | dev.tfvars, staging.tfvars, prod.tfvars in the root | -var-file="prod.tfvars" |
| Directory-per-env | environments/prod/{main.tf,prod.tfvars} | terraform -chdir=environments/prod ... |
The directory-per-environment layout is the safer choice for anything with real production traffic, because each environment also gets its own backend and state file — a botched apply in dev physically cannot reach prod. Whichever you pick, name the variable file after the environment it configures and keep the value of the environment variable matching the filename, so naming, tagging, and selection all agree.
Layering values
You rarely have just one axis of variation. A typical project has values that are global, per-environment, and per-developer. Terraform merges multiple -var-file flags left to right, with later files winning, which lets you compose them:
terraform apply \
-var-file="common.tfvars" \
-var-file="prod.tfvars"
Put company-wide defaults (tag set, region, project name) in common.tfvars, override only what differs in prod.tfvars, and keep the per-environment file small. This keeps the diff between two environments to the handful of values that genuinely change.
Secrets per environment
Secrets — database passwords, API tokens — must never live in committed tfvars files. Mark sensitive inputs so they are redacted from plan and CLI output, and source their values at runtime rather than from disk.
variable "db_password" {
type = string
sensitive = true
}
The most robust approach is to fetch secrets from a real secrets manager per environment, so the value never enters version control or local files at all:
data "aws_secretsmanager_secret_version" "db" {
secret_id = "${var.environment}/rds/password"
}
resource "aws_db_instance" "main" {
identifier = "${var.environment}-api"
engine = "postgres"
instance_class = "db.t3.medium"
username = "appuser"
password = data.aws_secretsmanager_secret_version.db.secret_string
}
For CI pipelines, environment variables are the simplest secure channel — Terraform reads any TF_VAR_<name> automatically:
export TF_VAR_db_password="$(vault kv get -field=password secret/prod/db)"
terraform apply -var-file="prod.tfvars"
Warning: Even a
sensitive = truevalue is stored in plaintext inside the state file. Always use an encrypted backend (S3 with SSE, plus restricted IAM) and a separate state per environment so production secrets are not readable by anyone with access to a lower environment’s state.
Validating required values
Catching a bad or missing value at plan time is far cheaper than discovering it after an apply. Use validation blocks to enforce constraints, and omit default on inputs that must be supplied so Terraform refuses to run without them.
variable "environment" {
type = string
description = "Deployment environment."
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be one of: dev, staging, prod."
}
}
Because the variable has no default, forgetting -var-file fails immediately:
Output:
╷
│ Error: No value for required variable
│
│ on variables.tf line 1:
│ 1: variable "environment" {
│
│ The root module input variable "environment" is not set, and has no
│ default value. Use a -var or -var-file command line argument to provide
│ a value for this variable.
╵
That hard failure is a feature: it makes “I forgot which environment I’m in” structurally impossible rather than a silent default.
Best Practices
- Keep
.tffiles identical across environments; express every difference as data in a per-environment*.tfvarsfile. - Avoid
*.auto.tfvarsfor environment selection — require an explicit-var-fileso the target is always a conscious choice. - Name the variable file after the environment and keep the
environmentvalue, filename, and tags in agreement. - Layer
common.tfvarsplus an environment file so each environment’s file contains only the values that truly differ. - Never commit secrets; mark them
sensitive = trueand source them from a secrets manager orTF_VAR_environment variables per environment. - Give required inputs no
defaultand addvalidationblocks so missing or invalid values fail at plan time, not after apply. - Isolate state per environment with an encrypted backend so a lower environment can never read production secrets.