env.dev

Terraform Variables: tfvars vs Environment Variables vs Variable Blocks

Compare all three approaches to Terraform variables with code examples. Variable blocks, .tfvars files, TF_VAR_ environment variables, precedence, sensitive values, and validation.

Last updated:

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).

hcl
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.

hcl
# 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 = 1

Use terraform.tfvars for shared defaults and -var-file for environment-specific overrides:

bash
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.

bash
export TF_VAR_region="ap-southeast-1"
export TF_VAR_instance_count=5
export TF_VAR_enable_monitoring=true

terraform plan

This 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:

bash
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:

  1. default value in the variable block
  2. terraform.tfvars file
  3. *.auto.tfvars files (in alphabetical order)
  4. TF_VAR_ environment variables
  5. -var-file flags (in the order specified)
  6. -var flags (in the order specified)

A -var flag on the CLI always wins. This makes it useful for one-off overrides without editing files.

bash
# 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.

hcl
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.

hcl
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.

hcl
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?

MethodBest forCommitted to Git?
default in variable blockSafe defaults that rarely changeYes
terraform.tfvarsShared team defaultsUsually
*.auto.tfvarsMachine-generated valuesSometimes
TF_VAR_ env varsCI/CD pipelines, secretsNo
-var-file flagPer-environment overridesYes (per env file)
-var flagOne-off overrides, debuggingNo

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:

text
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.tf

Key patterns to follow:

  • Keep all variable blocks in variables.tf so they are easy to discover
  • Always provide a description — it appears in terraform plan prompts and generated docs
  • Set type explicitly to catch assignment errors early
  • Use validation blocks for values that must match a pattern or a known set
  • Never commit .tfvars files that contain secrets — use TF_VAR_ environment variables or a secrets manager instead
  • Group related values into an object variable 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.

Frequently Asked Questions

What is the Terraform variable precedence order?

From lowest to highest: default value in the variable block, terraform.tfvars, *.auto.tfvars (alphabetical), -var-file arguments (in order), -var arguments (in order), TF_VAR_ environment variables. Later sources override earlier ones.

When should I use TF_VAR_ environment variables?

Use TF_VAR_ environment variables in CI/CD pipelines where you inject secrets at runtime, in containerized environments, and when you want to avoid storing sensitive values in files. Set them as TF_VAR_name=value.

How do I mark a Terraform variable as sensitive?

Add sensitive = true to the variable block. Terraform will redact the value from plan and apply output. Note: the value is still stored in state, so encrypt your state backend.

Was this helpful?

Stay up to date

Get notified about new guides, tools, and cheatsheets.