Back to Blog
TerraformIaCDevOpsAWS

Building Reusable Terraform Modules: A Complete Guide

John DoeJanuary 10, 20242 min read

Terraform modules are the key to writing maintainable, reusable infrastructure code. In this guide, I'll walk you through creating effective Terraform modules.

What Are Terraform Modules?

A module is a container for multiple resources that are used together. Every Terraform configuration has at least one module, known as the root module.

Module Structure

A well-organized module follows this structure:

modules/
└── vpc/
    ├── main.tf
    ├── variables.tf
    ├── outputs.tf
    ├── versions.tf
    └── README.md

Creating a VPC Module

Let's create a reusable VPC module for AWS:

variables.tf

variable "name" {
  description = "Name prefix for resources"
  type        = string
}

variable "cidr_block" {
  description = "CIDR block for the VPC"
  type        = string
  default     = "10.0.0.0/16"
}

variable "availability_zones" {
  description = "List of availability zones"
  type        = list(string)
}

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

main.tf

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

  tags = {
    Name = "${var.name}-vpc"
  }
}

resource "aws_subnet" "public" {
  count             = length(var.availability_zones)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.cidr_block, 8, count.index)
  availability_zone = var.availability_zones[count.index]

  map_public_ip_on_launch = true

  tags = {
    Name = "${var.name}-public-${count.index + 1}"
    Type = "public"
  }
}

resource "aws_subnet" "private" {
  count             = length(var.availability_zones)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.cidr_block, 8, count.index + 10)
  availability_zone = var.availability_zones[count.index]

  tags = {
    Name = "${var.name}-private-${count.index + 1}"
    Type = "private"
  }
}

outputs.tf

output "vpc_id" {
  description = "The 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 "private_subnet_ids" {
  description = "List of private subnet IDs"
  value       = aws_subnet.private[*].id
}

Using the Module

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

  name               = "production"
  cidr_block         = "10.0.0.0/16"
  availability_zones = ["us-west-2a", "us-west-2b", "us-west-2c"]
  enable_nat_gateway = true
}

output "vpc_id" {
  value = module.vpc.vpc_id
}

Best Practices

  1. Version your modules using semantic versioning
  2. Document everything with clear README files
  3. Use validation to catch errors early
  4. Keep modules focused on a single responsibility
  5. Test your modules with tools like Terratest

Conclusion

Well-designed modules save time, reduce errors, and make your infrastructure code a joy to work with. Start small and iterate!