Skip to content
Infrastructure as Code iac environments 5 min read

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.tfvars and any *.auto.tfvars file 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 files dev.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:

PatternLayoutSelection
Flat tfvars filesdev.tfvars, staging.tfvars, prod.tfvars in the root-var-file="prod.tfvars"
Directory-per-envenvironments/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 = true value 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 .tf files identical across environments; express every difference as data in a per-environment *.tfvars file.
  • Avoid *.auto.tfvars for environment selection — require an explicit -var-file so the target is always a conscious choice.
  • Name the variable file after the environment and keep the environment value, filename, and tags in agreement.
  • Layer common.tfvars plus an environment file so each environment’s file contains only the values that truly differ.
  • Never commit secrets; mark them sensitive = true and source them from a secrets manager or TF_VAR_ environment variables per environment.
  • Give required inputs no default and add validation blocks 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.
Last updated June 14, 2026
Was this helpful?