Terraform Modules â Reusable Infrastructure Code Guide
In this tutorial, you'll learn about Terraform Modules. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Terraform modules are self-contained packages of Terraform configuration that are used to create reusable, composable, and versioned infrastructure components across multiple projects and environments.
What You'll Learn
Why It Matters
Copy-pasting Terraform configuration across environments and projects leads to drift, inconsistencies, and maintenance nightmares. Terraform modules encapsulate infrastructure logic into reusable building blocks with defined inputs, outputs, and versioning. DodaTech uses 40+ reusable Terraform modules across AWS, GCP, and Azure, provisioning infrastructure consistently and reducing environment creation time from days to minutes.
Real-World Use
DodaZIP's cloud infrastructure is built from reusable modules â vpc, ecs-cluster, rds-postgres, redis, alb, and dns-zone. A new microservice environment is provisioned by composing these modules with environment-specific variables, all defined in a single root module.
flowchart TD
A[Root Module] --> B[Module: vpc]
A --> C[Module: ecs-cluster]
A --> D[Module: rds-postgres]
A --> E[Module: redis]
A --> F[Module: alb]
B --> G[AWS VPC]
C --> H[ECS Cluster + Auto Scaling]
D --> I[RDS PostgreSQL]
E --> J[ElastiCache Redis]
F --> K[Application Load Balancer]
style A fill:#844FBA,color:#fff
Prerequisites: Understanding of Terraform HCL syntax, state management, and cloud provider basics.
Module Structure
terraform-modules/
modules/
vpc/
main.tf # Core resources
variables.tf # Input variables
outputs.tf # Output values
versions.tf # Provider & version constraints
README.md # Documentation
rds-postgres/
main.tf
variables.tf
outputs.tf
ecs-service/
main.tf
variables.tf
outputs.tf
examples/
complete-vpc/
main.tf
terraform.tfvars
Writing a Module
# modules/vpc/variables.tf
variable "environment" {
description = "Environment name for resource tagging"
type = string
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be dev, staging, or production."
}
}
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
variable "public_subnet_cidrs" {
description = "CIDR blocks for public subnets"
type = list(string)
}
variable "private_subnet_cidrs" {
description = "CIDR blocks for private subnets"
type = list(string)
}
variable "availability_zones" {
description = "List of availability zones"
type = list(string)
}
variable "tags" {
description = "Additional resource tags"
type = map(string)
default = {}
}
# modules/vpc/main.tf
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge({
Name = "dodatech-${var.environment}-vpc"
Environment = var.environment
ManagedBy = "terraform"
}, var.tags)
}
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = merge({
Name = "dodatech-${var.environment}-public-${count.index + 1}"
Environment = var.environment
Type = "public"
}, var.tags)
}
resource "aws_subnet" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.private_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = false
tags = merge({
Name = "dodatech-${var.environment}-private-${count.index + 1}"
Environment = var.environment
Type = "private"
}, var.tags)
}
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = merge({
Name = "dodatech-${var.environment}-igw"
Environment = var.environment
}, var.tags)
}
resource "aws_eip" "nat" {
count = length(var.public_subnet_cidrs)
domain = "vpc"
tags = merge({
Name = "dodatech-${var.environment}-nat-${count.index + 1}"
Environment = var.environment
}, var.tags)
}
resource "aws_nat_gateway" "this" {
count = length(var.public_subnet_cidrs)
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = merge({
Name = "dodatech-${var.environment}-nat-${count.index + 1}"
Environment = var.environment
}, var.tags)
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
tags = merge({
Name = "dodatech-${var.environment}-public-rt"
Environment = var.environment
}, var.tags)
}
resource "aws_route_table_association" "public" {
count = length(var.public_subnet_cidrs)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.this[count.index].id
}
tags = merge({
Name = "dodatech-${var.environment}-private-rt-${count.index + 1}"
Environment = var.environment
}, var.tags)
}
resource "aws_route_table_association" "private" {
count = length(var.private_subnet_cidrs)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
# modules/vpc/outputs.tf
output "vpc_id" {
description = "The ID of the VPC"
value = aws_vpc.this.id
}
output "public_subnet_ids" {
description = "IDs of the public subnets"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "IDs of the private subnets"
value = aws_subnet.private[*].id
}
output "nat_gateway_ips" {
description = "Elastic IPs of the NAT gateways"
value = aws_eip.nat[*].public_ip
}
Using Modules
# root/main.tf
module "vpc" {
source = "git::https://github.com/dodatech/terraform-modules.git//modules/vpc?ref=v1.4.0"
environment = "production"
vpc_cidr = "10.0.0.0/16"
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
private_subnet_cidrs = ["10.0.10.0/24", "10.0.11.0/24", "10.0.12.0/24"]
tags = {
Project = "dodazip"
CostCenter = "platform-engineering"
}
}
module "rds" {
source = "git::https://github.com/dodatech/terraform-modules.git//modules/rds-postgres?ref=v2.1.0"
environment = "production"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
instance_class = "db.r6g.large"
allocated_storage = 200
engine_version = "16.3"
database_name = "dodazip"
master_username = "dodazip_admin"
backup_retention_period = 30
deletion_protection = true
multi_az = true
}
# Use outputs
output "vpc_id" {
value = module.vpc.vpc_id
}
output "database_endpoint" {
value = module.rds.endpoint
}
Module Versioning with Terraform Registry
terraform {
required_version = ">= 1.7.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
# Public module from registry
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.8.1"
name = "dodatech-production"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
enable_vpn_gateway = false
enable_dns_hostnames = true
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
Module Composition Pattern
# modules/ecs-service/variables.tf
variable "service_name" {
type = string
}
variable "environment" {
type = string
}
variable "vpc_id" {
type = string
}
variable "subnet_ids" {
type = list(string)
}
variable "container_image" {
type = string
}
variable "container_port" {
type = number
default = 8080
}
variable "desired_count" {
type = number
default = 2
}
# root/compose-service.tf
module "user_service" {
source = "./modules/ecs-service"
service_name = "user-service"
environment = "production"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
container_image = "registry.dodatech.com/user-service:latest"
container_port = 8080
desired_count = 3
}
module "auth_service" {
source = "./modules/ecs-service"
service_name = "auth-service"
environment = "production"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
container_image = "registry.dodatech.com/auth-service:latest"
container_port = 8081
desired_count = 2
}
Common Configuration Mistakes
Not pinning module versions: Using
source = "./modules/vpc"without a version tag means any change to the module affects all consuming projects. Pin to Git tags or registry versions withref=v1.0.0.Missing variable validation: Without
validationblocks invariables.tf, incorrect values (e.g., invalid instance types) only fail during apply. Validate early to fail fast.Hardcoding provider regions in modules: Modules should accept region as a variable, not hardcode it. This allows the same module to deploy to any region.
Ignoring output preconditions: Module consumers rely on correct outputs. Add
preconditionblocks to validate that outputs have expected values before returning.Not splitting state for large modules: A single massive root module slows down every plan and apply. Break infrastructure into separate state files per concern (networking, databases, compute).
Practice Questions
What is a Terraform module and why use it? Answer: A module is a directory of Terraform configuration files defining related infrastructure. Modules promote reusability, consistency, and versioning across projects.
How do you reference a module from a Git Repository? Answer: Use
source = "git::https://github.com/org/repo.git//path?ref=v1.0.0". The//pathselects a subdirectory,?refpins a version tag.What is the difference between root modules and child modules? Answer: The root module is the working directory where
<a href="/devops/terraform/">terraform</a> applyruns. Child modules are referenced viamoduleblocks from the root or other modules.How do you share outputs between modules? Answer: Use
module.module_name.output_nameto reference a child module's output from another module in the same root configuration.
Challenge
Create a Terraform module for a complete microservice deployment on AWS ECS Fargate: VPC with public/private subnets, ALB with path-based routing, ECS cluster with auto-scaling, RDS PostgreSQL with read replicas, ElastiCache Redis, and Route53 DNS. Write input variables with validation, output all resource identifiers, pin provider versions, and create an example root module that provisions two Microservices.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro