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)

  1. Default values in variable declarations
  2. Environment variables (TF_VAR_name)
  3. terraform.tfvars file
  4. terraform.tfvars.json file
  5. *.auto.tfvars files (alphabetical order)
  6. 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.