Terraform Modules

Modules are containers for multiple resources that are used together. They allow you to organize your code, reuse infrastructure, and create abstractions.

What are Modules?

Benefits of Modules

  • Reusability - Write once, use many times
  • Organization - Group related resources together
  • Abstraction - Hide complexity behind simple interfaces
  • Maintainability - Update in one place, affects all uses
  • Testing - Test modules independently

Module Structure

Standard Module Structure

modules/
└── vpc/
    ├── main.tf          # Main resources
    ├── variables.tf     # Input variables
    ├── outputs.tf       # Output values
    ├── README.md        # Documentation
    └── versions.tf      # Provider requirements

Creating a Module

Example: VPC Module

modules/vpc/variables.tf

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

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

variable "tags" {
  description = "Tags to apply to resources"
  type        = map(string)
  default     = {}
}

modules/vpc/main.tf

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = merge(var.tags, {
    Name        = "${var.environment}-vpc"
    Environment = var.environment
  })
}

resource "aws_subnet" "public" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = merge(var.tags, {
    Name = "${var.environment}-public-subnet-${count.index + 1}"
  })
}

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

modules/vpc/outputs.tf

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

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

output "vpc_cidr" {
  description = "CIDR block of the VPC"
  value       = aws_vpc.main.cidr_block
}

Using Modules

Local Modules

module "vpc" {
  source = "./modules/vpc"
  
  vpc_cidr     = "10.0.0.0/16"
  environment  = "production"
  
  tags = {
    Project = "web-app"
    ManagedBy = "Terraform"
  }
}

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

Git Repository Modules

module "vpc" {
  source = "git::https://github.com/example/terraform-aws-vpc.git"
  
  vpc_cidr = "10.0.0.0/16"
}

# Use specific branch/tag
module "vpc" {
  source = "git::https://github.com/example/terraform-aws-vpc.git?ref=v1.2.0"
}

# Use specific branch
module "vpc" {
  source = "git::https://github.com/example/terraform-aws-vpc.git?ref=main"
}

Terraform Registry Modules

# Public registry
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"
  
  name = "my-vpc"
  cidr = "10.0.0.0/16"
  
  azs             = ["us-east-1a", "us-east-1b"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]
  
  enable_nat_gateway = true
  enable_vpn_gateway = true
  
  tags = {
    Terraform   = "true"
    Environment = "production"
  }
}

Module Versioning

💡 Best Practice: Always pin module versions in production to ensure consistency.
# Pin to specific version
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"
}

# Use version constraints
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"  # >= 5.0, < 6.0
}

Module Composition

Nested Modules

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

module "ec2" {
  source = "./modules/ec2"
  
  vpc_id         = module.vpc.vpc_id
  subnet_ids     = module.vpc.public_subnet_ids
  security_groups = module.vpc.security_group_ids
}

Module Outputs

Accessing Module Outputs

# Module definition
output "instance_id" {
  value = aws_instance.web.id
}

# Using module output
resource "aws_eip" "web_ip" {
  instance = module.ec2.instance_id
}

# Pass output to another module
module "monitoring" {
  source = "./modules/monitoring"
  instance_id = module.ec2.instance_id
}

Module Best Practices

✅ Best Practices:
  • ✅ One module per logical component (VPC, EC2, RDS)
  • ✅ Use variables for all configurable values
  • ✅ Expose outputs for important resource attributes
  • ✅ Document modules with README.md
  • ✅ Use version constraints for module sources
  • ✅ Keep modules focused and single-purpose
  • ✅ Use local values for computed values
  • ✅ Follow consistent naming conventions

Module Examples

Complete Module Example

# modules/ec2/main.tf
variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t2.micro"
}

variable "subnet_id" {
  description = "Subnet ID for EC2 instance"
  type        = string
}

variable "tags" {
  description = "Tags for EC2 instance"
  type        = map(string)
  default     = {}
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type
  subnet_id     = var.subnet_id

  tags = var.tags
}

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}

output "instance_id" {
  description = "ID of the EC2 instance"
  value       = aws_instance.web.id
}

output "public_ip" {
  description = "Public IP of the EC2 instance"
  value       = aws_instance.web.public_ip
}

Next Steps