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.
📑 Table of Contents
- Input Variables
- Variable Declaration
- Variable Validation
- Sensitive Variables
- Nullable Variables
- Setting Variable Values
- Method 1: terraform.tfvars File
- Method 2: Environment-Specific Files
- Method 3: Command Line
- Method 4: Environment Variables
- Variable Precedence (Lowest to Highest)
- Local Values
- Output Values
- Accessing Outputs
- Terraform Modules
- Module Structure
- Creating a VPC Module
- Using the Module
- Module Sources
- Module Best Practices
- For Each with Modules
- Conclusion
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)
- Default values in variable declaration
- Environment variables (TF_VAR_*)
- terraform.tfvars file
- *.auto.tfvars files (alphabetical order)
- -var-file command line option
- -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
- Keep modules focused – One module per logical component
- Use semantic versioning – Tag releases properly
- Document thoroughly – Include README with examples
- Validate inputs – Use variable validation
- Expose necessary outputs – Make integration easy
- Use sensible defaults – Allow easy getting started
- 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?
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.