Terraform variables parameterize your infrastructure code so the same configuration can target dev, staging, and production without editing .tf files directly. Variables are declared in variable blocks, assigned through .tfvars files, environment variables, or CLI flags, and referenced with the var. prefix. Understanding the precedence rules and the difference between simple and complex types prevents misconfiguration that can destroy real infrastructure. This guide covers every mechanism Terraform provides for defining, assigning, validating, and organizing variables in a real project.
How do you declare a variable?
Every input variable requires a variable block. The block name becomes the reference key (var.name).
variable "region" {
type = string
default = "us-east-1"
description = "AWS region for all resources"
}
variable "instance_count" {
type = number
description = "Number of EC2 instances to create"
# No default — Terraform will prompt or require a value
}
variable "enable_monitoring" {
type = bool
default = true
description = "Whether to enable CloudWatch monitoring"
}The type argument accepts string, number, bool, and complex types covered later. When default is omitted, Terraform treats the variable as required.
How do you assign values with .tfvars files?
Terraform automatically loads two kinds of files: terraform.tfvars and any file matching *.auto.tfvars. For all other filenames, pass them explicitly with -var-file.
# terraform.tfvars — auto-loaded
region = "us-west-2"
instance_count = 3
# production.auto.tfvars — auto-loaded (alphabetical order)
enable_monitoring = true
# staging.tfvars — must be passed explicitly
# terraform plan -var-file="staging.tfvars"
region = "eu-west-1"
instance_count = 1Use terraform.tfvars for shared defaults and -var-file for environment-specific overrides:
terraform plan -var-file="environments/staging.tfvars"
terraform apply -var-file="environments/production.tfvars"How do environment variables work with Terraform?
Terraform reads any environment variable prefixed with TF_VAR_. The part after the prefix maps to the variable name.
export TF_VAR_region="ap-southeast-1"
export TF_VAR_instance_count=5
export TF_VAR_enable_monitoring=true
terraform planThis is the standard approach in CI/CD pipelines where you cannot use interactive prompts or commit secrets to version control. For complex types, pass JSON:
export TF_VAR_tags='{"Environment":"prod","Team":"platform"}'What is the variable precedence order?
When the same variable is set in multiple places, Terraform uses the last value it encounters, following this order from lowest to highest priority:
defaultvalue in the variable blockterraform.tfvarsfile*.auto.tfvarsfiles (in alphabetical order)TF_VAR_environment variables-var-fileflags (in the order specified)-varflags (in the order specified)
A -var flag on the CLI always wins. This makes it useful for one-off overrides without editing files.
# Overrides everything for this run
terraform apply -var="instance_count=1"How do you mark a variable as sensitive?
Setting sensitive = true tells Terraform to redact the value from CLI output and plan logs. It does not encrypt it in state.
variable "database_password" {
type = string
sensitive = true
description = "RDS master password"
}
resource "aws_db_instance" "main" {
password = var.database_password
# Terraform will show (sensitive value) in plan output
}Always combine sensitive = true with an external secrets source (environment variables, a secrets manager, or encrypted .tfvars files) rather than hardcoding values. Validate sensitive variables in your pipeline using a tool like the env validator.
What are the complex variable types?
Beyond primitives, Terraform supports list, map, set, object, and tuple types.
variable "availability_zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
variable "instance_tags" {
type = map(string)
default = {
Environment = "dev"
ManagedBy = "terraform"
}
}
variable "vpc_config" {
type = object({
cidr_block = string
enable_dns_support = bool
public_subnet_cidrs = list(string)
})
default = {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
}
}Use object when you need a structured configuration bag with known keys. Use map when keys are dynamic (like tags). Lists and sets are interchangeable in many contexts, but sets enforce uniqueness and have no guaranteed order.
How do validation blocks work?
The validation block runs a boolean condition and shows a custom error_message when the check fails. You can add multiple validation blocks per variable.
variable "environment" {
type = string
description = "Deployment environment"
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be dev, staging, or production."
}
}
variable "cidr_block" {
type = string
validation {
condition = can(cidrhost(var.cidr_block, 0))
error_message = "Must be a valid CIDR block, e.g. 10.0.0.0/16."
}
}
variable "instance_type" {
type = string
default = "t3.micro"
validation {
condition = startswith(var.instance_type, "t3.")
error_message = "Only t3 instance types are allowed."
}
}Validations run at plan time, catching errors before any infrastructure changes are applied.
When should you use which assignment method?
| Method | Best for | Committed to Git? |
|---|---|---|
| default in variable block | Safe defaults that rarely change | Yes |
| terraform.tfvars | Shared team defaults | Usually |
| *.auto.tfvars | Machine-generated values | Sometimes |
| TF_VAR_ env vars | CI/CD pipelines, secrets | No |
| -var-file flag | Per-environment overrides | Yes (per env file) |
| -var flag | One-off overrides, debugging | No |
How should you organize variables in a real project?
A common convention splits variable declarations into a dedicated file per concern and keeps assignments in environment-specific .tfvars files:
project/
├── main.tf # Resources and data sources
├── variables.tf # All variable declarations
├── outputs.tf # Output values
├── terraform.tfvars # Shared defaults
├── environments/
│ ├── dev.tfvars
│ ├── staging.tfvars
│ └── production.tfvars
└── modules/
└── vpc/
├── main.tf
├── variables.tf # Module-specific variables
└── outputs.tfKey patterns to follow:
- Keep all
variableblocks invariables.tfso they are easy to discover - Always provide a
description— it appears interraform planprompts and generated docs - Set
typeexplicitly to catch assignment errors early - Use
validationblocks for values that must match a pattern or a known set - Never commit
.tfvarsfiles that contain secrets — useTF_VAR_environment variables or a secrets manager instead - Group related values into an
objectvariable instead of creating many loosely related scalar variables
FAQ
Can a variable reference another variable?
No. Variable defaults must be literal values or simple expressions that do not reference other input variables. Use locals to derive computed values from multiple variables.
What happens if I set the same variable in terraform.tfvars and a .auto.tfvars file?
The .auto.tfvars value wins because it is loaded after terraform.tfvars. Among multiple .auto.tfvars files, they are processed in alphabetical order, so the last file alphabetically takes precedence.
Does sensitive = true encrypt the value in state?
No. It only redacts the value from CLI output and plan logs. The value is stored in plaintext in the state file. Always use a remote backend with encryption (such as S3 with server-side encryption) and restrict access to the state file.
How do I pass complex types through environment variables?
Set the TF_VAR_ value to a JSON-encoded string. For example, TF_VAR_tags='{"Env":"prod"}' assigns a map. Terraform automatically parses the JSON into the declared type.
For a quick reference of all Terraform commands and syntax, see the Terraform cheat sheet.