File & Environment #

Declaring variables is only half the work. The other half is the strategy for how those variable values are managed β€” in which files, by whom, how sensitive ones are secured, and how to make sure every environment gets the right values without excessive configuration duplication. Without a clear strategy, a multi-environment project quickly becomes a confusing pile of tfvars files.

flowchart TD
    A["πŸ“‹ Default value\nin the variable declaration"] -->|priority rises| B["πŸ“„ terraform.tfvars"]
    B -->|priority rises| C["πŸ“„ *.auto.tfvars\nread automatically"]
    C -->|priority rises| D["πŸ”§ TF_VAR_*\nenvironment variables"]
    D -->|priority rises| E["⚑ -var-file flag"]
    E -->|highest priority| F["⚑ -var flag"]

    style A fill:#6b7280,stroke:#4b5563,color:#fff
    style B fill:#3b82f6,stroke:#1e40af,color:#fff
    style C fill:#3b82f6,stroke:#1e40af,color:#fff
    style D fill:#f59e0b,stroke:#d97706,color:#fff
    style E fill:#f97316,stroke:#ea580c,color:#fff
    style F fill:#ef4444,stroke:#dc2626,color:#fff

The tfvars File Hierarchy #

Terraform reads variable files in a specific order. Understanding this hierarchy prevents confusion about which value ends up being used.

VARIABLE FILE READING ORDER (from lowest to highest):

1. default in the variable declaration
2. terraform.tfvars          (auto-loaded, if present)
3. terraform.tfvars.json     (auto-loaded, if present)
4. *.auto.tfvars             (auto-loaded, sorted alphabetically)
5. *.auto.tfvars.json        (auto-loaded, sorted alphabetically)
6. -var-file="file.tfvars"   (via flag, flag order determines priority)
7. -var="key=value"          (via flag, highest priority)

Values read later OVERWRITE earlier ones.
flowchart LR
    A["terraform.tfvars\nπŸ“Œ Always read"] --> C["Terraform\nEngine"]
    B["production.auto.tfvars\nπŸ“Œ Read automatically\nin alphabetical order"] --> C
    D["custom.tfvars\n⚑ Must be flagged\n-var-file"] --> C

    style A fill:#3b82f6,stroke:#1e40af,color:#fff
    style B fill:#10b981,stroke:#059669,color:#fff
    style C fill:#f59e0b,stroke:#d97706,color:#fff
    style D fill:#8b5cf6,stroke:#6d28d9,color:#fff

tfvars File Structure for Multi-Environment #

The cleanest pattern for multi-environment is one file per environment, selected at apply time with the -var-file flag.

environments/
  β”œβ”€β”€ dev.tfvars
  β”œβ”€β”€ staging.tfvars
  └── production.tfvars
# environments/dev.tfvars
environment    = "dev"
instance_type  = "t3.micro"
instance_count = 1

database_config = {
  instance_class    = "db.t3.micro"
  allocated_storage = 20
  multi_az          = false
  backup_retention  = 1
}

tags = {
  Environment = "dev"
  CostCenter  = "engineering"
  ManagedBy   = "terraform"
}
# environments/production.tfvars
environment    = "production"
instance_type  = "t3.medium"
instance_count = 3

database_config = {
  instance_class    = "db.r5.large"
  allocated_storage = 500
  multi_az          = true
  backup_retention  = 30
}

tags = {
  Environment = "production"
  CostCenter  = "engineering"
  ManagedBy   = "terraform"
}
# Use at apply time
terraform apply -var-file="environments/dev.tfvars"
terraform apply -var-file="environments/production.tfvars"

The terraform.tfvars File for Team Defaults #

terraform.tfvars is read automatically and is suitable for values that apply across all environments β€” defaults that don’t need to be overridden per environment.

# terraform.tfvars β€” default values, committed to Git
# (don't store secrets here)

region   = "ap-southeast-1"
owner    = "platform-team"

common_tags = {
  Project   = "my-app"
  ManagedBy = "terraform"
  Repository = "github.com/org/infrastructure"
}

# Values that are the same in every environment
enable_monitoring  = true
log_retention_days = 30

The TF_VAR_* Environment Variable #

Environment variables with the TF_VAR_ prefix are the right way to pass values that differ per context β€” especially in CI/CD pipelines and for secrets.

# Terraform reads TF_VAR_<variable_name> automatically
# The variable name must be an EXACT MATCH (case-sensitive)

export TF_VAR_environment="production"
export TF_VAR_instance_type="t3.medium"

# For variables with complex types, use the HCL/JSON format
export TF_VAR_database_config='{"instance_class":"db.r5.large","allocated_storage":500,"multi_az":true}'

terraform apply
# Terraform reads TF_VAR_* and uses them as variable values
# Usage example in GitHub Actions CI/CD
name: Terraform Apply Production

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3

      - name: Terraform Init
        run: terraform init

      - name: Terraform Apply
        env:
          # Non-sensitive values from the tfvars file
          TF_VAR_environment: "production"
          # Secrets from GitHub Secrets β€” never enter the code
          TF_VAR_db_password: ${{ secrets.DB_PASSWORD }}
          TF_VAR_api_key: ${{ secrets.API_KEY }}
          # AWS credentials
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: terraform apply -auto-approve -var-file="environments/production.tfvars"

Separating Secrets from Regular Configuration #

Secrets must never be committed to Git, even in .tfvars files. There are two main approaches.

# APPROACH 1: Read secrets from AWS Secrets Manager via a data source
# The secret is managed outside Terraform; Terraform only reads it

data "aws_secretsmanager_secret_version" "db_credentials" {
  secret_id = "production/database/credentials"
}

locals {
  db_credentials = jsondecode(
    data.aws_secretsmanager_secret_version.db_credentials.secret_string
  )
}

resource "aws_db_instance" "main" {
  username = local.db_credentials.username
  password = local.db_credentials.password
  # ... other attributes
}
# APPROACH 2: Sensitive variables filled via TF_VAR_* environment variables
# The secret is managed in the CI/CD secrets manager (GitHub Secrets, HashiCorp Vault)
# and passed to Terraform as an environment variable

variable "db_password" {
  type      = string
  sensitive = true
  # No default β€” Terraform will error if TF_VAR_db_password isn't set
}

resource "aws_db_instance" "main" {
  password = var.db_password
}

Anti-Patterns to Avoid #

# ANTI-PATTERN 1: Secrets in .tfvars files committed to Git
# production.tfvars
db_password = "super-secret-password"  # βœ— Leaks to everyone with repo access
api_key     = "Β«redacted:sk-…»"   # βœ— Stored in Git history forever

# ANTI-PATTERN 2: One tfvars file for all environments
# all-environments.tfvars
dev_instance_type  = "t3.micro"        # βœ— The file becomes big and confusing
prod_instance_type = "t3.medium"
dev_db_class       = "db.t3.micro"
prod_db_class      = "db.r5.large"
# Better: a separate file per environment

# ANTI-PATTERN 3: Hardcoded values in the configuration that should be variables
resource "aws_instance" "web" {
  instance_type = "t3.micro"   # βœ— Hardcoded β€” can't differ per environment
}
# Better: instance_type = var.instance_type


Using terraform.tfvars.json #

Besides the HCL format, Terraform also supports the JSON format for variable files.

// terraform.tfvars.json
{
  "environment": "production",
  "instance_type": "t3.medium",
  "instance_count": 3,
  "tags": {
    "Project": "MyApp",
    "ManagedBy": "Terraform"
  }
}
# terraform.tfvars.json is read automatically, same as terraform.tfvars
terraform plan

# The loading order for JSON is the same as HCL:
# 1. terraform.tfvars.json (if present, together with terraform.tfvars)
# 2. *.auto.tfvars.json (alphabetical)
# 3. -var-file flag

Environment-Specific Variable Files #

# Common pattern: one tfvars file per environment
# environments/
#   β”œβ”€β”€ dev.tfvars
#   β”œβ”€β”€ staging.tfvars
#   └── production.tfvars

terraform plan -var-file="environments/production.tfvars"
terraform apply -var-file="environments/staging.tfvars"

# In CI/CD, pick the file based on the branch or a parameter
if [ "$ENVIRONMENT" == "production" ]; then
  terraform apply -var-file="environments/production.tfvars" -auto-approve
elif [ "$ENVIRONMENT" == "staging" ]; then
  terraform apply -var-file="environments/staging.tfvars" -auto-approve
fi

Variable Precedence Deep Dive #

Terraform has a specific priority order for determining variable values.

VARIABLE PRIORITY (from low to high):

1. Default value in the variable declaration
   variable "x" { default = "low" }

2. terraform.tfvars (and terraform.tfvars.json)
   x = "medium-low"

3. *.auto.tfvars (and *.auto.tfvars.json) β€” alphabetical
   config.auto.tfvars: x = "medium"

4. -var-file flag
   terraform plan -var-file="custom.tfvars"
   x = "medium-high"

5. -var flag and TF_VAR_ environment variables
   terraform plan -var="x=high"
   TF_VAR_x="high" terraform plan

6. Input at the prompt (if there's no default)
   Terraform will ask for interactive input
# Example: override in CI/CD
# Default: terraform.tfvars β†’ environment = "dev"
# CI/CD override:
terraform plan -var="environment=staging"
# This overrides the value in terraform.tfvars

# Environment variable override:
export TF_VAR_instance_type="t3.large"
terraform plan
# TF_VAR_instance_type overrides all other sources

tfvars Security #

# DON'T commit tfvars files containing secrets to Git!

# .gitignore:
*.tfvars
*.tfvars.json
!terraform.tfvars.example

# For shared (non-sensitive) values:
# Commit terraform.tfvars.example with placeholders
# terraform.tfvars.example (commit this)
# environment = "dev"
# instance_type = "t3.micro"
# db_password = "CHANGE_ME"

# terraform.tfvars (DON'T commit)
environment  = "production"
instance_type = "m5.large"
db_password   = "actually-secret-password"

# Alternative: use environment variables for secrets
# export TF_VAR_db_password="secret"
# Terraform will read from the TF_VAR_ prefix

Hierarchical Variable Loading #

TERRAFORM VARIABLE LOADING ORDER:

1. Environment variables (TF_VAR_*)
   └── Loaded automatically from the shell

2. terraform.tfvars
   └── Auto-loaded if present in the working directory

3. *.auto.tfvars
   └── Auto-loaded, alphabetical order

4. -var-file=explicit.tfvars
   └── Explicit, can specify multiple

5. -var="key=value"
   └── Command line, highest priority

NAMING CONVENTION:
common.tfvars       β†’ Shared values across all envs
dev.tfvars          β†’ Dev-specific values
staging.tfvars      β†’ Staging-specific values
production.tfvars   β†’ Production-specific values
secrets.tfvars      β†’ NEVER commit (in .gitignore)
# Combine multiple var files
terraform apply   -var-file="common.tfvars"   -var-file="environments/dev.tfvars"   -var-file="regions/ap-southeast-1.tfvars"

Summary #

  • One tfvars file per environment (dev.tfvars, staging.tfvars, production.tfvars) selected with -var-file is the cleanest multi-environment pattern.
  • terraform.tfvars for team defaults that apply across all environments β€” committed to Git, no secrets.
  • TF_VAR_* environment variables for values that differ per context in CI/CD and for all secrets.
  • Secrets never go into tfvars files committed to Git β€” use TF_VAR_* from a secrets manager or read directly from AWS Secrets Manager via a data source.
  • Priority hierarchy: CLI flags > tfvars files > auto-loaded tfvars > env vars > defaults β€” more specific values always win.
  • Sensitive variables without defaults force values to always be provided explicitly β€” Terraform will error if there’s no value, preventing forgotten secrets.

← Previous: Type & Validation Β  Next: What is an Output? β†’

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact