Skip to content
All essays
ArchitectureJanuary 17, 202418 min

Terraform Modules: Building Reusable Infrastructure

Master Terraform modules for reusable infrastructure. Learn to create, use, and share modules for scalable and maintainable IaC implementations.

Ü
Ümit Uz
Mobile & Full Stack Developer

Understanding Terraform Modules

Terraform modules are self-contained packages of Terraform configurations that are managed as a group. They enable you to reuse code, reduce duplication, and maintain consistency across your infrastructure.

Why Use Modules?

Benefits:

  • Reusability: Write once, use everywhere
  • Organization: Group related resources together
  • Consistency: Standardize infrastructure patterns
  • Maintainability: Update in one place, affect all consumers
  • Abstraction: Hide complexity behind simple interfaces

Module Structure

Basic Module Layout

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

Creating Your First Module

Module Structure:

modules/
└── web-server/
    ├── main.tf
    ├── variables.tf
    ├── outputs.tf
    └── README.md

main.tf (Module Code):

hcl
resource "aws_instance" "web" {
  count         = var.instance_count
  ami           = var.ami_id
  instance_type = var.instance_type

  subnet_id = var.subnet_ids[count.index % length(var.subnet_ids)]

  vpc_security_group_ids = [var.security_group_id]

  user_data = var.user_data

  tags = merge(
    var.common_tags,
    {
      Name = "${var.instance_name}-${count.index}"
    }
  )
}

resource "aws_lb_target_group_attachment" "web" {
  count            = var.instance_count
  target_group_arn = var.target_group_arn
  target_id        = aws_instance.web[count.index].id
  port             = 80
}

variables.tf (Module Inputs):

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

variable "ami_id" {
  description = "AMI ID to use for instances"
  type        = string
}

variable "instance_type" {
  description = "Instance type"
  type        = string
  default     = "t2.micro"
}

variable "subnet_ids" {
  description = "List of subnet IDs"
  type        = list(string)
}

variable "security_group_id" {
  description = "Security group ID"
  type        = string
}

variable "instance_name" {
  description = "Base name for instances"
  type        = string
}

variable "user_data" {
  description = "User data script"
  type        = string
  default     = null
}

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

variable "target_group_arn" {
  description = "Target group ARN for load balancer"
  type        = string
}

outputs.tf (Module Outputs):

hcl
output "instance_ids" {
  description = "IDs of created instances"
  value       = aws_instance.web[*].id
}

output "instance_public_ips" {
  description = "Public IPs of instances"
  value       = aws_instance.web[*].public_ip
}

output "instance_private_ips" {
  description = "Private IPs of instances"
  value       = aws_instance.web[*].private_ip
}

Using Modules

Local Module Usage

Calling a Local Module:

hcl
# main.tf
module "web_server" {
  source = "./modules/web-server"

  instance_count    = 3
  ami_id            = var.ami_id
  instance_type     = "t2.micro"
  subnet_ids        = module.vpc.public_subnet_ids
  security_group_id = module.security_group.sg_id
  instance_name     = "web"
  common_tags       = var.common_tags
  target_group_arn  = module.load_balancer.target_group_arn
  user_data         = file("${path.module}/scripts/web-setup.sh")
}

Remote Module Usage

Public Registry Module:

hcl
module "vpc" {
  source = "terraform-aws-modules/vpc/aws"

  name = "my-vpc"
  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
  single_nat_gateway = true

  tags = {
    Terraform   = "true"
    Environment = "dev"
  }
}

GitHub Source:

hcl
module "network" {
  source = "github.com/myorg/terraform-modules//network"

  vpc_cidr   = "10.0.0.0/16"
  aws_region = "us-east-1"
}

S3 Source:

hcl
module "storage" {
  source = "s3::https://s3.amazonaws.com/my-bucket/modules/storage.zip"

  bucket_prefix = "my-app-"
  environment   = "production"
}

Module Versions

Version Constraints

hcl
module "vpc" {
  source = "terraform-aws-modules/vpc/aws"
  version = ">= 3.0, < 4.0"
}

Version Operators

  • >= 3.0: Any version 3.0 or higher
  • ~> 3.0: Any version 3.x.x
  • = 3.0.0: Exact version 3.0.0
  • > 3.0, < 4.0: Any version between 3.0 and 4.0

Module Patterns

1. Complete Infrastructure Module

hcl
# modules/web-infrastructure/main.tf
resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr

  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = var.tags
}

resource "aws_subnet" "public" {
  count = length(var.availability_zones)

  vpc_id                  = aws_vpc.main.id
  cidr_block              = cidrsubnet(var.vpc_cidr, 8, count.index)
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true

  tags = merge(var.tags, {
    Name = "${var.name}-public-${count.index}"
  })
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = var.tags
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = var.tags
}

resource "aws_route_table_association" "public" {
  count = length(var.availability_zones)

  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

2. Multi-Environment Module

hcl
# modules/environment/main.tf
module "vpc" {
  source = "../vpc"

  name      = "${var.environment}-vpc"
  vpc_cidr  = var.vpc_cidr
  tags      = var.common_tags
}

module "ec2" {
  source = "../ec2"

  vpc_id             = module.vpc.vpc_id
  subnet_ids         = module.vpc.public_subnet_ids
  instance_count     = var.instance_count
  instance_type      = var.instance_type
  environment        = var.environment
  common_tags        = var.common_tags
}

module "rds" {
  source = "../rds"

  vpc_id         = module.vpc.vpc_id
  subnet_ids     = module.vpc.private_subnet_ids
  engine         = var.database_engine
  engine_version = var.database_version
  instance_class = var.database_instance_class
  environment    = var.environment
  common_tags    = var.common_tags
}

3. Composite Module Pattern

hcl
# modules/web-app-cluster/main.tf
module "asg" {
  source = "../autoscaling-group"

  vpc_id          = var.vpc_id
  subnet_ids      = var.subnet_ids
  instance_type   = var.instance_type
  min_size        = var.min_size
  max_size        = var.max_size
  desired_capacity = var.desired_capacity
}

module "alb" {
  source = "../application-load-balancer"

  vpc_id     = var.vpc_id
  subnet_ids = var.subnet_ids
}

module "dns" {
  source = "../route53"

  domain_name = var.domain_name
  alb_dns_name = module.alb.dns_name
}

Module Development Best Practices

1. Document Your Modules

README.md Template:

markdown
# Web Server Module

## Description
Creates a scalable web server infrastructure with Auto Scaling.

## Requirements
| Name | Version |
|------|---------|
| terraform | >= 1.0 |
| aws | >= 4.0 |

## Providers
| Name | Version |
|------|---------|
| aws | >= 4.0 |

## Inputs
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|----------|
| instance_count | Number of instances | number | 1 | no |
| instance_type | EC2 instance type | string | t2.micro | no |

## Outputs
| Name | Description |
|------|-------------|
| instance_ids | List of instance IDs |
| alb_dns_name | DNS name of the load balancer |

## Usage

module "web" { source = "./modules/web-server" instance_count = 3 }

2. Use Validation Rules

hcl
variable "instance_type" {
  description = "EC2 instance type"
  type        = string

  validation {
    condition     = can(regex("^t[23]\.(micro|small|medium)$", var.instance_type))
    error_message = "Instance type must be t2.micro, t2.small, t2.medium, t3.micro, t3.small, or t3.medium."
  }
}

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

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

3. Provide Sensible Defaults

hcl
variable "enable_monitoring" {
  description = "Enable detailed monitoring"
  type        = bool
  default     = true
}

variable "common_tags" {
  description = "Common tags for all resources"
  type        = map(string)
  default     = {
    ManagedBy = "Terraform"
  }
}

4. Use Local Values

hcl
locals {
  name_prefix = "${var.project}-${var.environment}"
  common_tags = {
    Project     = var.project
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

resource "aws_instance" "web" {
  tags = merge(local.common_tags, var.extra_tags)
}

Module Registry

Publishing to Terraform Registry

  1. 1Structure Your Repository
terraform-aws-module/
├── modules/
│   └── example/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
├── examples/
│   └── complete/
│       └── main.tf
├── README.md
├── LICENSE
└── terraform.yml
  1. 1Create terraform.yml
yaml
name: my_awesome_module
namespace: myorg
description: An awesome Terraform module
provider: aws
version: 1.0.0
  1. 1Publish
  • Push to GitHub/GitLab
  • Register at registry.terraform.io

Using Registry Modules

hcl
module "db" {
  source = "terraform-aws-modules/rds/aws"
  version = "5.0.0"

  identifier = "mydb"

  engine            = "mysql"
  engine_version    = "5.7"
  instance_class    = "db.t2.micro"
  allocated_storage = 20

  name     = "mydb"
  username = "admin"
  password = var.db_password

  vpc_security_group_ids = [module.security_group.security_group_id]

  tags = {
    Owner = "me"
  }
}

Module Testing

1. Structure Tests

ruby
# test/terraform/modules/example_test.rb
describe 'Modules' do
  describe 'VPC Module' do
    it 'creates a VPC' do
      expect(vpc).to exist
    end

    it 'has correct CIDR block' do
      expect(vpc.cidr_block).to eq('10.0.0.0/16')
    end
  end
end

2. Integration Tests

hcl
# examples/complete/main.tf
module "test" {
  source = "../../modules/vpc"

  vpc_cidr = "10.0.0.0/16"
  tags = {
    Test = "true"
  }
}
bash
cd examples/complete
terraform init
terraform apply -auto-approve
terraform destroy -auto-approve

Advanced Module Techniques

1. Dynamic Blocks

hcl
module "security_group" {
  source = "./modules/security-group"

  ingress_rules = [
    {
      description = "HTTP"
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      description = "HTTPS"
      from_port   = 443
      to_port     = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  ]
}

2. Optional Resources

hcl
variable "enable_nat_gateway" {
  description = "Enable NAT Gateway"
  type        = bool
  default     = false
}

resource "aws_nat_gateway" "main" {
  count = var.enable_nat_gateway ? 1 : 0

  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[0].id
}

3. Module Composition

hcl
module "production" {
  source = "./modules/environment"

  environment = "production"
  vpc_cidr    = "10.0.0.0/16"
}

module "staging" {
  source = "./modules/environment"

  environment = "staging"
  vpc_cidr    = "10.1.0.0/16"
}

Common Pitfalls

1. Hardcoded Values

Avoid:

hcl
resource "aws_instance" "web" {
  ami = "ami-12345678"  # Hardcoded
}

Prefer:

hcl
variable "ami_id" {
  description = "AMI ID"
  type        = string
}

resource "aws_instance" "web" {
  ami = var.ami_id
}

2. Over-Abstraction

Too Generic:

hcl
variable "resource_properties" {
  type = map(string)
}

Better:

hcl
variable "instance_type" {
  type = string
}

variable "root_volume_size" {
  type = number
}

3. Tight Coupling

Design modules to be loosely coupled and composable.

Module Maintenance

Versioning Strategy

bash
terraform init -upgrade
terraform plan
terraform apply

Breaking Changes

  1. 1Increment major version
  2. 2Document changes in CHANGELOG.md
  3. 3Provide migration guide

Real-World Examples

Multi-Region Deployment

hcl
module "us-east" {
  source = "./modules/infrastructure"

  region = "us-east-1"
  name   = "production"
}

module "eu-west" {
  source = "./modules/infrastructure"

  region = "eu-west-1"
  name   = "production"
}

Blue-Green Deployment

hcl
module "blue" {
  source = "./modules/deployment"

  color       = "blue"
  instance_id = "i-blue123"
}

module "green" {
  source = "./modules/deployment"

  color       = "green"
  instance_id = "i-green456"
}

Conclusion

Terraform modules are essential for building maintainable infrastructure as code. They promote reuse, consistency, and collaboration. Start simple, document well, and iterate based on usage patterns.

Remember: Good modules are focused, well-documented, and thoroughly tested. They abstract complexity while maintaining flexibility.

Enjoyed this essay? Share it.

Related essays

Next essay
Terraform Basics: Infrastructure as Code Fundamentals