Terraform Variables, Outputs, and Modules: Complete Guide to Reusable Infrastructure

Master Terraform variables, outputs, and modules to create flexible, reusable, and maintainable infrastructure code. This guide covers input variables, output values, local values, and how to build and use modules effectively.

Input Variables

Variables allow you to parameterize your Terraform configurations, making them flexible and reusable across different environments.

Variable Declaration

# variables.tf

# String variable with default
variable "environment" {
  description = "Deployment environment"
  type        = string
  default     = "dev"
}

# Number variable
variable "instance_count" {
  description = "Number of instances to create"
  type        = number
  default     = 1
}

# Boolean variable
variable "enable_monitoring" {
  description = "Enable detailed monitoring"
  type        = bool
  default     = false
}

# List variable
variable "availability_zones" {
  description = "List of availability zones"
  type        = list(string)
  default     = ["us-east-1a", "us-east-1b"]
}

# Map variable
variable "instance_tags" {
  description = "Tags for instances"
  type        = map(string)
  default = {
    Team    = "DevOps"
    Project = "Infrastructure"
  }
}

# Object variable (structured)
variable "database_config" {
  description = "Database configuration"
  type = object({
    engine         = string
    instance_class = string
    storage_gb     = number
    multi_az       = bool
  })
  default = {
    engine         = "mysql"
    instance_class = "db.t3.micro"
    storage_gb     = 20
    multi_az       = false
  }
}

# Set variable (unique values)
variable "allowed_ports" {
  description = "Allowed ingress ports"
  type        = set(number)
  default     = [22, 80, 443]
}

# Tuple variable (fixed length, mixed types)
variable "server_config" {
  description = "Server name and port"
  type        = tuple([string, number])
  default     = ["webserver", 8080]
}

Variable Validation

variable "environment" {
  description = "Environment name"
  type        = string
  
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
  
  validation {
    condition     = can(regex("^t[23]\\.", var.instance_type))
    error_message = "Instance type must be t2 or t3 series."
  }
}

variable "vpc_cidr" {
  description = "VPC CIDR block"
  type        = string
  
  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "Must be a valid CIDR block."
  }
}

Sensitive Variables

variable "db_password" {
  description = "Database password"
  type        = string
  sensitive   = true  # Hides value in output
}

variable "api_key" {
  description = "API key for external service"
  type        = string
  sensitive   = true
}

Nullable Variables

variable "optional_config" {
  description = "Optional configuration"
  type        = string
  default     = null
  nullable    = true
}

Setting Variable Values

Method 1: terraform.tfvars File

# terraform.tfvars
environment       = "prod"
instance_count    = 3
enable_monitoring = true
availability_zones = ["us-west-2a", "us-west-2b", "us-west-2c"]

instance_tags = {
  Team        = "Production"
  Project     = "MainApp"
  CostCenter  = "12345"
}

Method 2: Environment-Specific Files

# dev.tfvars
environment    = "dev"
instance_count = 1
instance_type  = "t3.micro"

# prod.tfvars
environment    = "prod"
instance_count = 5
instance_type  = "t3.large"

# Apply with specific file
terraform apply -var-file="prod.tfvars"

Method 3: Command Line

terraform apply -var="environment=prod" -var="instance_count=3"

Method 4: Environment Variables

export TF_VAR_environment="prod"
export TF_VAR_db_password="secret123"
terraform apply

Variable Precedence (Lowest to Highest)

  1. Default values in variable declaration
  2. Environment variables (TF_VAR_*)
  3. terraform.tfvars file
  4. *.auto.tfvars files (alphabetical order)
  5. -var-file command line option
  6. -var command line option

Local Values

Locals are named expressions for values used multiple times within a module.

locals {
  # Simple values
  project_name = "myapp"
  environment  = var.environment
  
  # Computed values
  name_prefix = "${local.project_name}-${local.environment}"
  
  # Common tags
  common_tags = {
    Project     = local.project_name
    Environment = local.environment
    ManagedBy   = "Terraform"
    CreatedAt   = timestamp()
  }
  
  # Conditional logic
  instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
  
  # Complex expressions
  subnet_cidrs = [for i in range(3) : cidrsubnet(var.vpc_cidr, 8, i)]
}

# Using locals
resource "aws_instance" "web" {
  instance_type = local.instance_type
  
  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-web"
  })
}

Output Values

Outputs expose values from your configuration for use by other configurations or for display.

# outputs.tf

# Simple output
output "vpc_id" {
  description = "The ID of the VPC"
  value       = aws_vpc.main.id
}

# Sensitive output
output "db_password" {
  description = "Database password"
  value       = var.db_password
  sensitive   = true
}

# Conditional output
output "load_balancer_dns" {
  description = "Load balancer DNS name"
  value       = var.enable_lb ? aws_lb.main[0].dns_name : null
}

# List output
output "instance_ips" {
  description = "Public IPs of all instances"
  value       = aws_instance.web[*].public_ip
}

# Map output
output "instance_details" {
  description = "Details of all instances"
  value = {
    for instance in aws_instance.web :
    instance.id => {
      public_ip  = instance.public_ip
      private_ip = instance.private_ip
      az         = instance.availability_zone
    }
  }
}

# Complex object output
output "infrastructure_summary" {
  description = "Summary of deployed infrastructure"
  value = {
    vpc_id          = aws_vpc.main.id
    public_subnets  = aws_subnet.public[*].id
    private_subnets = aws_subnet.private[*].id
    instance_count  = length(aws_instance.web)
  }
}

Accessing Outputs

# View all outputs
terraform output

# View specific output
terraform output vpc_id

# Get raw value (for scripts)
terraform output -raw vpc_id

# Get as JSON
terraform output -json

Terraform Modules

Modules are containers for multiple resources that are used together. They enable code reuse and organization.

Module Structure

modules/
└── vpc/
    ├── main.tf       # Resource definitions
    ├── variables.tf  # Input variables
    ├── outputs.tf    # Output values
    └── README.md     # Documentation

Creating a VPC Module

modules/vpc/variables.tf:

variable "vpc_cidr" {
  description = "CIDR block for VPC"
  type        = string
}

variable "environment" {
  description = "Environment name"
  type        = string
}

variable "public_subnet_count" {
  description = "Number of public subnets"
  type        = number
  default     = 2
}

variable "private_subnet_count" {
  description = "Number of private subnets"
  type        = number
  default     = 2
}

variable "enable_nat_gateway" {
  description = "Enable NAT Gateway for private subnets"
  type        = bool
  default     = false
}

modules/vpc/main.tf:

data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true
  
  tags = {
    Name        = "vpc-${var.environment}"
    Environment = var.environment
  }
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id
  
  tags = {
    Name = "igw-${var.environment}"
  }
}

resource "aws_subnet" "public" {
  count                   = var.public_subnet_count
  vpc_id                  = aws_vpc.this.id
  cidr_block              = cidrsubnet(var.vpc_cidr, 8, count.index)
  availability_zone       = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true
  
  tags = {
    Name = "subnet-public-${count.index + 1}-${var.environment}"
    Type = "Public"
  }
}

resource "aws_subnet" "private" {
  count             = var.private_subnet_count
  vpc_id            = aws_vpc.this.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
  availability_zone = data.aws_availability_zones.available.names[count.index]
  
  tags = {
    Name = "subnet-private-${count.index + 1}-${var.environment}"
    Type = "Private"
  }
}

modules/vpc/outputs.tf:

output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.this.id
}

output "vpc_cidr" {
  description = "VPC CIDR block"
  value       = aws_vpc.this.cidr_block
}

output "public_subnet_ids" {
  description = "List of public subnet IDs"
  value       = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  description = "List of private subnet IDs"
  value       = aws_subnet.private[*].id
}

output "internet_gateway_id" {
  description = "Internet Gateway ID"
  value       = aws_internet_gateway.this.id
}

Using the Module

# main.tf in root module

module "vpc_dev" {
  source = "./modules/vpc"
  
  vpc_cidr            = "10.0.0.0/16"
  environment         = "dev"
  public_subnet_count = 2
  private_subnet_count = 2
  enable_nat_gateway  = false
}

module "vpc_prod" {
  source = "./modules/vpc"
  
  vpc_cidr            = "10.1.0.0/16"
  environment         = "prod"
  public_subnet_count = 3
  private_subnet_count = 3
  enable_nat_gateway  = true
}

# Access module outputs
resource "aws_instance" "web" {
  subnet_id = module.vpc_dev.public_subnet_ids[0]
  # ...
}

output "dev_vpc_id" {
  value = module.vpc_dev.vpc_id
}

output "prod_vpc_id" {
  value = module.vpc_prod.vpc_id
}

Module Sources

# Local path
module "vpc" {
  source = "./modules/vpc"
}

# GitHub
module "vpc" {
  source = "github.com/myorg/terraform-modules//vpc?ref=v1.0.0"
}

# Terraform Registry
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"
}

# S3 bucket
module "vpc" {
  source = "s3::https://s3-eu-west-1.amazonaws.com/bucket/vpc.zip"
}

# Git over SSH
module "vpc" {
  source = "git@github.com:myorg/modules.git//vpc?ref=v1.0.0"
}

Module Best Practices

  1. Keep modules focused – One module per logical component
  2. Use semantic versioning – Tag releases properly
  3. Document thoroughly – Include README with examples
  4. Validate inputs – Use variable validation
  5. Expose necessary outputs – Make integration easy
  6. Use sensible defaults – Allow easy getting started
  7. Test modules – Use Terratest or similar tools

For Each with Modules

variable "environments" {
  default = {
    dev = {
      vpc_cidr     = "10.0.0.0/16"
      subnet_count = 2
    }
    staging = {
      vpc_cidr     = "10.1.0.0/16"
      subnet_count = 2
    }
    prod = {
      vpc_cidr     = "10.2.0.0/16"
      subnet_count = 3
    }
  }
}

module "vpcs" {
  for_each = var.environments
  source   = "./modules/vpc"
  
  environment         = each.key
  vpc_cidr            = each.value.vpc_cidr
  public_subnet_count = each.value.subnet_count
}

output "vpc_ids" {
  value = { for k, v in module.vpcs : k => v.vpc_id }
}

Conclusion

Mastering variables, outputs, and modules is essential for writing maintainable Terraform code. Use variables for flexibility, outputs for integration, and modules for reusability. Following these patterns will help you build infrastructure that scales with your organization.

Was this article helpful?

R

About Ramesh Sundararamaiah

Red Hat Certified Architect

Expert in Linux system administration, DevOps automation, and cloud infrastructure. Specializing in Red Hat Enterprise Linux, CentOS, Ubuntu, Docker, Ansible, and enterprise IT solutions.

🐧 Stay Updated with Linux Tips

Get the latest tutorials, news, and guides delivered to your inbox weekly.

Add Comment