Terraform Variables and Outputs — Make Your Code Reusable
Hardcoding an AMI ID, an instance type, and a region directly into your Terraform config works exactly once. The moment you need a second environment — staging, production, a different region — you are copying and pasting entire files. Variables and outputs exist to stop that cycle.
Input Variables — The Basics
An input variable is declared with a variable block. Think of it as a function parameter for your Terraform configuration.
variable "instance_type" {
description = "EC2 instance type for the web server"
type = string
default = "t3.micro"
}
variable "environment" {
description = "Deployment environment"
type = string
# No default — Terraform will prompt for this value
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = var.instance_type
tags = {
Name = "web-${var.environment}"
Environment = var.environment
}
}
When you run terraform apply without providing environment, Terraform asks for it interactively. That is great for learning, terrible for CI/CD.
Variable Types
Terraform supports several types beyond simple strings.
# String
variable "region" {
type = string
default = "us-east-1"
}
# Number
variable "instance_count" {
type = number
default = 2
}
# Boolean
variable "enable_monitoring" {
type = bool
default = true
}
# List of strings
variable "availability_zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
# Map of strings
variable "instance_tags" {
type = map(string)
default = {
Team = "platform"
Project = "api-gateway"
}
}
# Object with specific structure
variable "database_config" {
type = object({
engine = string
engine_version = string
instance_class = string
storage_gb = number
multi_az = bool
})
default = {
engine = "postgres"
engine_version = "15.4"
instance_class = "db.t3.micro"
storage_gb = 20
multi_az = false
}
}
Use these typed variables in resources:
resource "aws_db_instance" "main" {
engine = var.database_config.engine
engine_version = var.database_config.engine_version
instance_class = var.database_config.instance_class
allocated_storage = var.database_config.storage_gb
multi_az = var.database_config.multi_az
username = "admin"
password = var.db_password
}
Variable Precedence — Who Wins?
This is the question that confuses everyone. If you set the same variable in multiple places, which value does Terraform use? Here is the precedence order from lowest to highest:
| Priority | Source | Example |
|---|---|---|
| 1 (lowest) | Default value in variable block | default = "t3.micro" |
| 2 | Environment variable | export TF_VAR_instance_type="t3.small" |
| 3 | terraform.tfvars file | instance_type = "t3.medium" |
| 4 | *.auto.tfvars files (alphabetical) | prod.auto.tfvars |
| 5 | -var-file flag | terraform apply -var-file="prod.tfvars" |
| 6 (highest) | -var flag on command line | terraform apply -var="instance_type=t3.large" |
Higher priority overrides lower priority. The -var flag always wins.
terraform.tfvars vs auto.tfvars
The terraform.tfvars file is automatically loaded by Terraform. It is the standard place to set variable values:
# terraform.tfvars — automatically loaded
region = "us-east-1"
environment = "production"
instance_type = "t3.medium"
instance_count = 3
availability_zones = [
"us-east-1a",
"us-east-1b",
"us-east-1c",
]
Any file ending in .auto.tfvars is also automatically loaded, in alphabetical order. This is useful for separating concerns:
# network.auto.tfvars
vpc_cidr = "10.0.0.0/16"
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
# compute.auto.tfvars
instance_type = "t3.medium"
instance_count = 3
For environment-specific configs, use named .tfvars files and pass them explicitly:
# Development
terraform apply -var-file="environments/dev.tfvars"
# Production
terraform apply -var-file="environments/prod.tfvars"
Locals — Computed Values
Locals are not inputs. They are computed values that help you avoid repeating expressions throughout your code.
locals {
# Combine variables into computed values
name_prefix = "${var.project}-${var.environment}"
# Build a common tags map to attach to every resource
common_tags = {
Environment = var.environment
Project = var.project
ManagedBy = "terraform"
Owner = var.team_email
}
# Conditional logic
instance_type = var.environment == "production" ? "t3.large" : "t3.micro"
}
resource "aws_instance" "app" {
ami = var.ami_id
instance_type = local.instance_type
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-app"
Role = "application"
})
}
resource "aws_s3_bucket" "assets" {
bucket = "${local.name_prefix}-assets"
tags = local.common_tags
}
When to use locals vs variables: Use variables when a value should be set by the caller (different per environment). Use locals when a value is derived from other values within the configuration.
Variable Validation Rules
You can enforce rules on variable values so that invalid input fails fast during plan, not during apply when a cloud API rejects it.
variable "environment" {
type = string
description = "Deployment environment"
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be one of: dev, staging, production."
}
}
variable "instance_type" {
type = string
description = "EC2 instance type"
validation {
condition = can(regex("^t3\\.", var.instance_type))
error_message = "Only t3 instance types are allowed for cost control."
}
}
variable "cidr_block" {
type = string
description = "VPC CIDR block"
validation {
condition = can(cidrhost(var.cidr_block, 0))
error_message = "Must be a valid CIDR notation (e.g., 10.0.0.0/16)."
}
}
Sensitive Variables
Mark a variable as sensitive to prevent Terraform from displaying its value in plan output and logs.
variable "db_password" {
type = string
description = "Database master password"
sensitive = true
}
# Terraform will show: db_password = (sensitive value)
Note that marking a variable as sensitive does not encrypt it in state. The value still appears in terraform.tfstate as plain text. Always encrypt your state backend.
Output Values
Outputs expose values from your configuration. They are essential for two things: displaying useful information after apply and passing data between modules.
output "instance_public_ip" {
description = "Public IP of the web server"
value = aws_instance.web.public_ip
}
output "database_endpoint" {
description = "RDS database connection endpoint"
value = aws_db_instance.main.endpoint
}
output "database_password" {
description = "Database password (sensitive)"
value = aws_db_instance.main.password
sensitive = true
}
output "load_balancer_dns" {
description = "DNS name of the load balancer"
value = aws_lb.main.dns_name
# Only output this if the LB actually exists
depends_on = [aws_lb.main]
}
After terraform apply, you see:
Outputs:
instance_public_ip = "54.210.123.45"
database_endpoint = "mydb.abc123.us-east-1.rds.amazonaws.com:5432"
database_password = <sensitive>
load_balancer_dns = "my-alb-1234567890.us-east-1.elb.amazonaws.com"
Passing Outputs Between Modules
This is where outputs become truly powerful. A networking module can export VPC and subnet IDs that a compute module consumes:
# modules/networking/outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
# root main.tf
module "networking" {
source = "./modules/networking"
vpc_cidr = var.vpc_cidr
environment = var.environment
}
module "compute" {
source = "./modules/compute"
vpc_id = module.networking.vpc_id
subnet_ids = module.networking.private_subnet_ids
instance_type = var.instance_type
environment = var.environment
}
This is how real-world Terraform projects are structured. Each module is self-contained, with clear inputs (variables) and outputs.
Wrapping Up
Variables, locals, and outputs form the foundation of reusable Terraform code. Without them, every project is a one-off snowflake. With them, you can deploy the same infrastructure across dev, staging, and production with a single -var-file flag.
In the next post, we will dive into Terraform providers — the plugins that connect Terraform to AWS, Azure, GCP, Kubernetes, and over 3000 other services.
