Terraform Variables and Outputs: A Complete Guide for Infrastructure Teams
Introduction
Managing infrastructure configuration effectively requires a deep understanding of Terraform's variable system. Variables and outputs form the backbone of reusable, maintainable infrastructure code, enabling teams to deploy consistent environments while adapting to different requirements and constraints.
Modern infrastructure teams face increasing pressure to deliver reliable, scalable solutions quickly. The ability to parameterize infrastructure code through variables while maintaining security and compliance standards has become essential. This guide explores how to leverage Terraform's variable system effectively, with insights into advanced patterns that enterprise teams use to manage complex deployments.
Understanding Input Variables
Input variables serve as the parameters for your Terraform modules, creating a clear interface between configuration consumers and the underlying infrastructure resources. They enable code reusability while maintaining type safety and validation.
Basic Variable Declaration
Every variable in Terraform follows a consistent structure that defines its interface:
variable "environment" {
description = "The deployment environment (dev, staging, prod)"
type = string
default = "dev"
sensitive = false
nullable = false
}
variable "instance_count" {
description = "Number of instances to create"
type = number
default = 1
validation {
condition = var.instance_count > 0 && var.instance_count <= 10
error_message = "Instance count must be between 1 and 10."
}
}
The key components of a variable declaration include the name, description for documentation, type constraint for validation, optional default value, and sensitivity marking for secure handling.
Variable Reference Syntax
Variables are referenced throughout your configuration using the var.<name>
syntax:
resource "aws_instance" "web" {
count = var.instance_count
ami = var.ami_id
instance_type = var.instance_type
tags = {
Name = "${var.project_name}-${var.environment}-web-${count.index + 1}"
Environment = var.environment
}
}
This syntax provides clear traceability of where configuration values originate, making code easier to understand and maintain.
Variable Types and Constraints
Terraform's type system helps prevent configuration errors by enforcing data structure requirements before deployment. Understanding the available types enables more robust module interfaces.
Primitive Types
The three primitive types handle basic data requirements:
variable "project_name" {
type = string
description = "Name of the project"
}
variable "enable_monitoring" {
type = bool
description = "Whether to enable monitoring"
default = true
}
variable "max_size" {
type = number
description = "Maximum size in GB"
default = 100
}
Collection Types
Collection types organize multiple values with consistent structure:
variable "availability_zones" {
type = list(string)
description = "List of availability zones"
default = ["us-west-2a", "us-west-2b", "us-west-2c"]
}
variable "instance_types" {
type = set(string)
description = "Set of allowed instance types"
default = ["t3.micro", "t3.small", "t3.medium"]
}
variable "region_configs" {
type = map(string)
description = "Configuration values by region"
default = {
"us-west-2" = "ami-12345"
"us-east-1" = "ami-67890"
}
}
Structural Types
Structural types define complex data structures with mixed types:
variable "database_config" {
type = object({
engine = string
engine_version = string
instance_class = string
allocated_storage = number
backup_retention = number
multi_az = bool
tags = map(string)
})
description = "Database configuration object"
}
variable "server_specifications" {
type = tuple([string, number, bool])
description = "Server specs: [instance_type, disk_size, monitoring_enabled]"
default = ["t3.micro", 20, true]
}
Variable Validation Rules
Validation rules enforce business logic and prevent invalid configurations from reaching the deployment phase. They provide immediate feedback during the planning stage.
Basic Validation Examples
variable "environment" {
type = string
description = "Deployment environment"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be one of: dev, staging, prod."
}
}
variable "instance_type" {
type = string
description = "EC2 instance type"
validation {
condition = can(regex("^[tm][0-9]", var.instance_type))
error_message = "Instance type must start with 't' or 'm' followed by a number."
}
}
Advanced Cross-Variable Validation
With Terraform 1.9+, validation rules can reference other variables, enabling sophisticated validation logic:
variable "create_database" {
type = bool
description = "Whether to create a database"
default = false
}
variable "database_password" {
type = string
description = "Database password"
sensitive = true
default = ""
validation {
condition = var.create_database == false || (
var.create_database == true && length(var.database_password) >= 8
)
error_message = "Database password must be at least 8 characters when creating a database."
}
}
Local Values for Code Simplification
Local values reduce repetition and improve code readability by assigning names to expressions that are used multiple times throughout a configuration.
Basic Local Values
locals {
# Environment-specific settings
environment_configs = {
dev = {
instance_type = "t3.micro"
min_size = 1
max_size = 2
}
prod = {
instance_type = "m5.large"
min_size = 2
max_size = 10
}
}
# Current environment configuration
config = local.environment_configs[var.environment]
# Common tags applied to all resources
common_tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "Terraform"
CreatedAt = timestamp()
}
}
Advanced Local Expressions
Local values excel at transforming data and implementing complex conditional logic:
locals {
# Transform list to map for easier lookups
subnet_map = { for idx, subnet in var.subnet_ids : idx => subnet }
# Conditional resource naming
resource_prefix = var.environment == "prod" ? var.project_name : "${var.project_name}-${var.environment}"
# Complex conditional logic
backup_schedule = var.environment == "prod" ? "0 2 * * *" : (
var.environment == "staging" ? "0 3 * * 0" : "0 4 * * 6"
)
# Flattened list for dynamic resource creation
security_rules = flatten([
for sg_name, sg_config in var.security_groups : [
for rule in sg_config.rules : {
sg_name = sg_name
type = rule.type
from_port = rule.from_port
to_port = rule.to_port
protocol = rule.protocol
cidr_blocks = rule.cidr_blocks
}
]
])
}
Output Values and Data Sharing
Output values expose information from your Terraform configuration, serving as the return values that other configurations or users can consume.
Basic Output Declarations
output "instance_public_ip" {
description = "Public IP address of the web server"
value = aws_instance.web.public_ip
}
output "database_endpoint" {
description = "RDS instance endpoint"
value = aws_db_instance.main.endpoint
sensitive = true
}
output "load_balancer_dns" {
description = "DNS name of the load balancer"
value = aws_lb.main.dns_name
}
Structured Output Values
For complex information, return structured data that's easier to consume:
output "infrastructure_info" {
description = "Complete infrastructure information"
value = {
vpc = {
id = aws_vpc.main.id
cidr = aws_vpc.main.cidr_block
}
instances = [
for instance in aws_instance.web : {
id = instance.id
public_ip = instance.public_ip
az = instance.availability_zone
}
]
database = {
endpoint = aws_db_instance.main.endpoint
port = aws_db_instance.main.port
}
}
}
Conditional Outputs
Outputs can be conditional based on resource creation:
output "backup_bucket_name" {
description = "S3 bucket name for backups"
value = var.enable_backups ? aws_s3_bucket.backup[0].bucket : null
}
Variable Precedence and Sources
Terraform loads variable values from multiple sources with a defined precedence order, providing flexibility in how configurations are supplied.
Precedence Order (lowest to highest)
- Default values in variable declarations
- Environment variables (
TF_VAR_name
) - terraform.tfvars file
- terraform.tfvars.json file
- *.auto.tfvars files (alphabetical order)
- Command line
-var
and-var-file
flags
Environment Variables
Environment variables provide a secure way to supply sensitive values:
# Linux/macOS
export TF_VAR_database_password="supersecret123"
export TF_VAR_api_key="abc123def456"
# Complex types as JSON
export TF_VAR_tags='{"Environment":"prod","Owner":"DevOps"}'
export TF_VAR_subnets='["subnet-12345","subnet-67890"]'
Variable Files
Variable files organize configuration values by environment or purpose:
# production.tfvars
environment = "prod"
instance_type = "m5.large"
instance_count = 5
enable_monitoring = true
enable_backups = true
database_config = {
engine = "mysql"
engine_version = "8.0"
instance_class = "db.r5.xlarge"
allocated_storage = 1000
backup_retention = 30
multi_az = true
tags = {
Backup = "required"
Tier = "production"
}
}
Command Line Variables
Command line options provide immediate overrides for testing or one-time deployments:
terraform apply \
-var="environment=staging" \
-var="instance_count=3" \
-var-file="staging.tfvars"
Managing Variables Across Environments
Effective variable management becomes critical as infrastructure complexity grows. Organizations need patterns that scale across multiple environments while maintaining security and compliance.
Environment-Specific Configuration Pattern
# variables.tf
variable "environment" {
description = "Target environment"
type = string
}
variable "region" {
description = "AWS region"
type = string
default = "us-west-2"
}
# locals.tf
locals {
# Environment-specific configurations
env_configs = {
dev = {
instance_type = "t3.micro"
min_capacity = 1
max_capacity = 2
enable_logging = false
backup_retention = 7
}
staging = {
instance_type = "t3.small"
min_capacity = 2
max_capacity = 4
enable_logging = true
backup_retention = 14
}
prod = {
instance_type = "m5.large"
min_capacity = 3
max_capacity = 20
enable_logging = true
backup_retention = 30
}
}
# Select configuration for current environment
config = local.env_configs[var.environment]
# Standardized resource naming
name_prefix = "${var.project_name}-${var.environment}"
}
This pattern provides a centralized location for environment-specific settings while maintaining consistency across deployments. Organizations using Scalr can leverage this pattern effectively through their policy-as-code framework, ensuring that environment-specific constraints are automatically enforced.
Complex Variable Types in Practice
Real-world infrastructure often requires sophisticated data structures to capture complex relationships and configurations.
Multi-Environment Service Configuration
variable "services" {
description = "Service configurations for different environments"
type = map(object({
image_tag = string
replicas = number
cpu_limit = string
memory_limit = string
environment_vars = map(string)
health_check = object({
enabled = bool
path = string
initial_delay = number
timeout_seconds = number
})
scaling = object({
min_replicas = number
max_replicas = number
cpu_threshold = number
})
}))
default = {
web = {
image_tag = "latest"
replicas = 2
cpu_limit = "500m"
memory_limit = "512Mi"
environment_vars = {
LOG_LEVEL = "info"
PORT = "8080"
}
health_check = {
enabled = true
path = "/health"
initial_delay = 30
timeout_seconds = 10
}
scaling = {
min_replicas = 2
max_replicas = 10
cpu_threshold = 70
}
}
}
}
Network Configuration with Validation
variable "network_config" {
description = "Network configuration for VPC and subnets"
type = object({
vpc_cidr = string
subnets = list(object({
name = string
cidr = string
availability_zone = string
public = bool
tags = map(string)
}))
})
validation {
condition = can(cidr_host(var.network_config.vpc_cidr, 0))
error_message = "VPC CIDR must be a valid CIDR block."
}
validation {
condition = alltrue([
for subnet in var.network_config.subnets :
can(cidr_subnet(var.network_config.vpc_cidr, 4, 0))
])
error_message = "All subnet CIDRs must be valid subnets of the VPC CIDR."
}
}
Conditional Logic with Variables
Terraform's conditional expressions enable dynamic infrastructure that adapts to different requirements and environments.
Resource Creation Conditions
# Conditional resource creation using count
resource "aws_cloudwatch_dashboard" "monitoring" {
count = var.environment == "prod" ? 1 : 0
dashboard_name = "${local.name_prefix}-dashboard"
dashboard_body = jsonencode({
widgets = [
{
type = "metric"
properties = {
metrics = [
["AWS/EC2", "CPUUtilization", "InstanceId", aws_instance.web[0].id]
]
region = var.region
title = "EC2 CPU Utilization"
}
}
]
})
}
# Conditional resource creation using for_each
resource "aws_s3_bucket" "backup" {
for_each = var.enable_backups ? { backup = true } : {}
bucket = "${local.name_prefix}-backup-${random_id.bucket_suffix.hex}"
tags = merge(local.common_tags, {
Purpose = "Backup"
})
}
Environment-Dependent Resource Configuration
resource "aws_autoscaling_group" "web" {
name = "${local.name_prefix}-asg"
vpc_zone_identifier = var.subnet_ids
target_group_arns = [aws_lb_target_group.web.arn]
min_size = local.config.min_capacity
max_size = local.config.max_capacity
desired_capacity = local.config.min_capacity
# Production gets more sophisticated health checks
health_check_type = var.environment == "prod" ? "ELB" : "EC2"
health_check_grace_period = var.environment == "prod" ? 300 : 120
# Only production gets scheduled scaling
dynamic "tag" {
for_each = var.environment == "prod" ? [1] : []
content {
key = "ScheduledScaling"
value = "enabled"
propagate_at_launch = false
}
}
launch_template {
id = aws_launch_template.web.id
version = "$Latest"
}
}
Complex Conditional Logic in Locals
locals {
# Multi-condition logic for backup configuration
backup_config = var.environment == "prod" ? {
enabled = true
retention_days = 30
frequency = "daily"
cross_region = true
} : var.environment == "staging" ? {
enabled = true
retention_days = 14
frequency = "weekly"
cross_region = false
} : {
enabled = false
retention_days = 7
frequency = "never"
cross_region = false
}
# Security group rules based on environment
security_rules = concat(
# Base rules for all environments
[
{
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTP"
}
],
# Additional rules for production
var.environment == "prod" ? [
{
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS"
}
] : [],
# Development-specific rules
var.environment == "dev" ? [
{
type = "ingress"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.office_cidr]
description = "SSH from office"
}
] : []
)
}
Best Practices for Variable Organization
Well-organized variables make infrastructure code more maintainable and reduce the likelihood of configuration errors. Following consistent patterns benefits the entire team.
File Structure Organization
terraform-project/
├── variables.tf # All input variable declarations
├── locals.tf # Local value definitions
├── outputs.tf # Output value declarations
├── main.tf # Primary resource definitions
├── versions.tf # Provider and Terraform version constraints
├── terraform.tfvars # Default variable values
├── environments/
├── dev.tfvars # Development-specific values
├── staging.tfvars # Staging-specific values
└── prod.tfvars # Production-specific values
Variable Documentation Standards
variable "database_configuration" {
description = <<-EOT
Complete database configuration including engine settings, capacity,
backup configuration, and security settings. The instance_class should
be sized appropriately for the expected workload.
Example:
database_configuration = {
engine = "mysql"
engine_version = "8.0"
instance_class = "db.r5.large"
storage = {
allocated = 100
max_allocated = 1000
type = "gp2"
encrypted = true
}
backup = {
retention_period = 7
window = "03:00-04:00"
final_snapshot = true
}
}
EOT
type = object({
engine = string
engine_version = string
instance_class = string
storage = object({
allocated = number
max_allocated = number
type = string
encrypted = bool
})
backup = object({
retention_period = number
window = string
final_snapshot = bool
})
})
validation {
condition = contains([
"mysql", "postgres", "mariadb", "oracle-ee", "sqlserver-ex"
], var.database_configuration.engine)
error_message = "Database engine must be a supported RDS engine type."
}
validation {
condition = var.database_configuration.storage.allocated >= 20
error_message = "Minimum allocated storage is 20 GB."
}
}
Naming Conventions
# Resource-specific variables use descriptive prefixes
variable "vpc_cidr_block" {
type = string
}
variable "subnet_availability_zones" {
type = list(string)
}
variable "security_group_ingress_rules" {
type = list(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
}
# Feature flags use enable_ prefix
variable "enable_monitoring" {
type = bool
default = false
}
variable "enable_backup_encryption" {
type = bool
default = true
}
# Configuration objects use _config suffix
variable "logging_config" {
type = object({
level = string
retention_days = number
cloudwatch_logs = bool
})
}
Teams using platforms like Scalr benefit from these naming conventions as they integrate seamlessly with policy frameworks and provide clear audit trails for compliance requirements.
Summary Table
Component | Purpose | Key Features | Best Practices |
---|---|---|---|
Input Variables | Define module parameters | Type constraints, validation, defaults, sensitivity | Use descriptive names, include documentation, apply validation rules |
Local Values | Simplify complex expressions | Computed values, conditional logic, data transformation | Group related calculations, avoid overuse, document complex logic |
Output Values | Expose module information | Structured data, sensitivity handling, conditional outputs | Return useful information, use structured data, mark sensitive outputs |
Variable Files | Environment-specific configuration | .tfvars files, automatic loading, precedence |
Organize by environment, secure sensitive values, document structure |
Environment Variables | Runtime configuration | TF_VAR_ prefix, complex type support |
Use for sensitive data, CI/CD integration, avoid in production logs |
Type Constraints | Prevent configuration errors | Primitive, collection, and structural types | Match complexity to use case, validate early, provide clear error messages |
Validation Rules | Enforce business logic | Custom conditions, cross-variable validation | Validate at variable level, provide helpful error messages, test thoroughly |
Conditional Logic | Dynamic configuration | Ternary operators, count/for_each, null values | Keep conditions readable, use locals for complex logic, document assumptions |
Conclusion
Mastering Terraform variables and outputs enables infrastructure teams to build flexible, maintainable, and secure infrastructure as code. The patterns and practices outlined in this guide provide a foundation for scaling infrastructure operations while maintaining consistency and reliability.
The combination of properly structured variables, comprehensive validation, and thoughtful output design creates infrastructure code that adapts to changing requirements without sacrificing reliability. Organizations that implement these patterns often find that their infrastructure becomes more predictable and easier to manage across multiple environments.
For teams looking to enhance their Terraform workflows further, platforms like Scalr provide additional capabilities around policy enforcement, cost management, and collaborative workflows that complement these variable management practices. The investment in proper variable design pays dividends as infrastructure complexity grows and teams scale their operations.
Whether you're managing a simple web application or complex multi-region deployments, the principles of effective variable management remain consistent: clear interfaces, comprehensive validation, and organized structure that supports your team's operational requirements.