Deep Dive: Terraform Variable Precedence
In production Terraform projects, the same variable can be defined in multiple places. Terraform follows a clear precedence order to decide which value should be used. Understanding this is very important for DevOps engineers because many production issues happen when Terraform picks a different variable value than expected.
| Priority | Source | Example |
|---|---|---|
| Highest | Command line variable | terraform apply -var="environment=prod" |
| High | Variable file passed manually | terraform apply -var-file="prod.tfvars" |
| Medium | Auto-loaded tfvars | prod.auto.tfvars |
| Medium | terraform.tfvars | environment = "dev" |
| Low | Environment variable | TF_VAR_environment=stage |
| Lowest | Default value | default = "dev" |
Production Warning
If your production pipeline accidentally uses dev.tfvars instead of prod.tfvars,
Terraform may create smaller instances, wrong tags, wrong CIDR ranges, or deploy resources into the wrong region.
Always print selected workspace, environment name, and variable file in CI/CD logs before running apply.
Variable Validation in Terraform
Variable validation helps prevent wrong infrastructure values before Terraform reaches the provider API. This is extremely useful in production because it catches mistakes early.
variable "environment" {
description = "Allowed deployment environment"
type = string
validation {
condition = contains(["dev", "qa", "stage", "prod"], var.environment)
error_message = "Environment must be one of: dev, qa, stage, prod."
}
}
variable "instance_type" {
description = "Allowed EC2 instance type"
type = string
validation {
condition = contains(["t3.micro", "t3.small", "t3.medium", "m5.large"], var.instance_type)
error_message = "Instance type is not approved for this project."
}
}
This is very important for companies in the USA, UK, and India where cloud cost control, governance, and compliance are major concerns.
Using Object Variables for Production Infrastructure
Instead of creating many separate variables, production Terraform modules often use object variables. Object variables group related values together and make modules cleaner.
variable "app_config" {
description = "Application infrastructure configuration"
type = object({
app_name = string
environment = string
instance_type = string
min_size = number
max_size = number
enable_https = bool
})
}
Example prod.tfvars:
app_config = {
app_name = "payment-service"
environment = "prod"
instance_type = "m5.large"
min_size = 3
max_size = 10
enable_https = true
}
Using Map Variables for Environment-Based Configuration
Map variables are useful when different environments need different values.
variable "instance_types" {
type = map(string)
default = {
dev = "t3.micro"
stage = "t3.small"
prod = "m5.large"
}
}
resource "aws_instance" "app" {
ami = var.ami_id
instance_type = var.instance_types[var.environment]
}
This keeps your Terraform code reusable across development, staging, and production environments.
Using Locals for Standard Naming and Tags
Locals are very useful for enforcing naming standards. In enterprise projects, naming standards are important for billing, monitoring, security, and operations.
locals {
name_prefix = "${var.project}-${var.environment}-${var.region}"
common_tags = {
Project = var.project
Environment = var.environment
Owner = var.owner
ManagedBy = "Terraform"
CostCenter = var.cost_center
}
}
resource "aws_s3_bucket" "logs" {
bucket = "${local.name_prefix}-logs"
tags = local.common_tags
}
Sensitive Variables in Terraform
Sensitive variables hide values from normal Terraform CLI output, but they are still stored in Terraform state. This is very important to understand.
variable "db_password" {
description = "Database password"
type = string
sensitive = true
}
resource "aws_db_instance" "main" {
identifier = "prod-db"
username = "admin"
password = var.db_password
}
Important Security Note
sensitive = true only hides values in Terraform output. It does not remove secrets from state.
Use encrypted remote state, strict IAM permissions, Terraform Cloud sensitive variables, Vault, AWS Secrets Manager,
Azure Key Vault, or GCP Secret Manager for production secrets.
Production-Ready Variables File Structure
terraform-project/
│
├── main.tf
├── variables.tf
├── outputs.tf
├── locals.tf
├── providers.tf
│
├── env/
│ ├── dev.tfvars
│ ├── stage.tfvars
│ └── prod.tfvars
│
└── README.md
dev.tfvars
environment = "dev"
instance_type = "t3.micro"
min_size = 1
max_size = 2
prod.tfvars
environment = "prod"
instance_type = "m5.large"
min_size = 3
max_size = 10
Outputs for Real Production Projects
Outputs are useful for showing important deployment information and passing values between modules or remote states.
output "load_balancer_dns" {
description = "Application Load Balancer DNS name"
value = aws_lb.app.dns_name
}
output "vpc_id" {
description = "Created VPC ID"
value = aws_vpc.main.id
}
output "private_subnet_ids" {
description = "Private subnet IDs"
value = aws_subnet.private[*].id
}
Using Outputs Between Modules
Outputs become very powerful when working with modules.
module "network" {
source = "./modules/network"
environment = var.environment
}
module "app" {
source = "./modules/app"
vpc_id = module.network.vpc_id
subnet_ids = module.network.private_subnet_ids
}
Here, the application module depends on outputs from the network module. This is how real Terraform projects connect networking, compute, database, security, and monitoring modules.
Remote State Outputs
In large teams, networking may be managed in one Terraform state file and applications in another. Remote state allows one Terraform project to read outputs from another.
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "company-terraform-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_instance" "app" {
ami = var.ami_id
instance_type = var.instance_type
subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0]
}
Real-Time Scenario: Wrong Variable Caused Production Issue
A DevOps engineer deployed infrastructure using:
terraform apply -var-file="dev.tfvars"
But the target workspace was production. Terraform created smaller instances, disabled backups, and applied development tags in production. This caused performance issues and monitoring confusion.
How to Prevent This
variable "environment" {
type = string
validation {
condition = var.environment == terraform.workspace
error_message = "Variable environment must match Terraform workspace."
}
}
You can also add CI/CD checks to ensure the correct variable file is used for each environment.
Best Practices for Terraform Variables and Outputs
- Always define variable types.
- Use descriptions for every variable.
- Use validation blocks for important values.
- Never hardcode secrets in
variables.tf. - Use
sensitive = truefor secrets. - Use separate
.tfvarsfiles for dev, stage, and prod. - Do not commit secret tfvars files to Git.
- Use locals for repeated naming and tagging logic.
- Use outputs to expose important infrastructure values.
- Mark sensitive outputs as sensitive.
- Use remote state outputs carefully.
- Document required variables in README.
Terraform State and State Files
Understand how variables, outputs, and sensitive values are stored in Terraform state.
Remote State and State Locking
Learn how production teams securely store Terraform state and share outputs.
Reusable Terraform Modules
Use variables and outputs to build reusable production-grade Terraform modules.