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

  1. Always define variable types.
  2. Use descriptions for every variable.
  3. Use validation blocks for important values.
  4. Never hardcode secrets in variables.tf.
  5. Use sensitive = true for secrets.
  6. Use separate .tfvars files for dev, stage, and prod.
  7. Do not commit secret tfvars files to Git.
  8. Use locals for repeated naming and tagging logic.
  9. Use outputs to expose important infrastructure values.
  10. Mark sensitive outputs as sensitive.
  11. Use remote state outputs carefully.
  12. 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.