Wednesday, November 5, 2025

How to Create and Use Terraform Modules for Reusable Code

How to Create and Use Terraform Modules for Reusable Code
Terraform Modules Reusability Infrastructure as Code DevOps

How to Create and Use Terraform Modules for Reusable Code

Published on: November 9, 2023 | Author: DevOps Engineering Team

Terraform Modules: Build Once, Use Everywhere

Welcome to Part 6 of our Terraform Mastery Series! You've learned about variables and outputs. Now it's time to level up your infrastructure code with modules. Terraform modules let you package and reuse configurations across projects and teams, making your infrastructure code more maintainable, scalable, and consistent.

What are Modules and Why Use Them?

Modules are containers for multiple Terraform resources that are used together. Think of them as functions in programming - they encapsulate logic and can be reused with different inputs.

Reusability

Write once, use everywhere. Create standardized infrastructure patterns that can be reused across projects and teams.

Abstraction

Hide complexity behind simple interfaces. Consumers don't need to understand the implementation details.

Organization

Break down complex infrastructure into logical, manageable components that are easier to understand and maintain.

Module Analogy

Think of modules like LEGO blocks. You can create standardized components (modules) and combine them in different ways to build complex structures (infrastructure) without reinventing the wheel each time.

Understanding the Root Module

Every Terraform configuration has at least one module - the root module. This is the directory where you run terraform apply.

my-terraform-project/
├── main.tf          # Primary resources
├── variables.tf     # Input variables
├── outputs.tf       # Output values
├── terraform.tfvars # Variable values
└── terraform.tfstate # State file (generated)

The root module can call other modules (child modules) to compose more complex infrastructure:

my-terraform-project/
├── main.tf
├── variables.tf
├── outputs.tf
└── modules/
    ├── networking/
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    └── compute/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

Calling Public Modules from Terraform Registry

The Terraform Registry hosts thousands of pre-built modules you can use in your configurations.

1

Find a Module

Browse the Terraform Registry for modules. For example, the AWS VPC module:

terraform-aws-modules/vpc/aws
2

Call the Module

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"
  version = "~> 3.0"
  
  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
  enable_vpn_gateway = true
  
  tags = {
    Terraform = "true"
    Environment = "dev"
  }
}
3

Use Module Outputs

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

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

Benefits of Public Modules

  • Community tested: Used and validated by thousands of users
  • Best practices: Implement AWS and Terraform best practices
  • Time saving: Avoid reinventing common patterns
  • Versioned: Pin to specific versions for stability

Creating Your Own Local Modules

Creating your own modules helps standardize infrastructure patterns within your organization.

1

Create Module Directory Structure

my-project/
├── main.tf
├── variables.tf
├── outputs.tf
└── modules/
    └── web-server/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf
2

Define Module Interface (Variables)

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

variable "instance_type" {
  type        = string
  description = "EC2 instance type"
  default     = "t3.micro"
}

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

variable "vpc_id" {
  type        = string
  description = "VPC ID where instances will be created"
}

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

Implement Module Logic

resource "aws_security_group" "web" {
  name_prefix = "${var.name}-web-"
  vpc_id      = var.vpc_id
  
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  tags = {
    Name = "${var.name}-web-sg"
  }
}

resource "aws_instance" "web" {
  count = var.instance_count
  
  ami           = "ami-0c02fb55956c7d316" # Amazon Linux 2
  instance_type = var.instance_type
  subnet_id     = var.subnet_ids[count.index % length(var.subnet_ids)]
  
  vpc_security_group_ids = [aws_security_group.web.id]
  
  user_data = file("${path.module}/user-data.sh")
  
  tags = {
    Name = "${var.name}-web-${count.index}"
  }
}
4

Define Module Outputs

output "instance_ids" {
  value = aws_instance.web.*.id
}

output "instance_private_ips" {
  value = aws_instance.web.*.private_ip
}

output "security_group_id" {
  value = aws_security_group.web.id
}

Module Structure and Best Practices

A well-structured module follows consistent patterns that make it easy to use and maintain.

main.tf

Purpose: Contains the primary resource definitions

Contains: AWS resources, data sources, providers

Best Practice: Keep it focused on the module's core purpose

variables.tf

Purpose: Defines the module's input interface

Contains: Input variable declarations with types and descriptions

Best Practice: Provide sensible defaults when possible

outputs.tf

Purpose: Defines what the module exposes to callers

Contains: Output values for important resources and computed values

Best Practice: Document outputs with descriptions

README.md

Purpose: Documentation for module users

Contains: Usage examples, input/output reference, requirements

Best Practice: Include complete usage examples

Module Design Principles

  • Single Responsibility: Each module should do one thing well
  • Clear Interface: Well-defined inputs and outputs
  • Documentation: Clear usage instructions and examples
  • Testing: Include examples and tests for different use cases

Passing Inputs to Modules

Modules accept inputs through variables, allowing customization for different use cases.

module "web_servers" {
  source = "./modules/web-server"
  
  name           = "production"
  instance_type = "t3.large"
  instance_count = 3
  vpc_id         = module.vpc.vpc_id
  subnet_ids     = module.vpc.private_subnets
}

You can use the same module with different inputs for different environments:

# Development environment
module "web_dev" {
  source = "./modules/web-server"
  
  name           = "dev"
  instance_type = "t3.micro"
  instance_count = 1
  vpc_id         = module.vpc.vpc_id
  subnet_ids     = module.vpc.private_subnets
}

# Production environment
module "web_prod" {
  source = "./modules/web-server"
  
  name           = "prod"
  instance_type = "t3.large"
  instance_count = 3
  vpc_id         = module.vpc.vpc_id
  subnet_ids     = module.vpc.private_subnets
}

Using Module Outputs

Module outputs allow you to pass information from child modules to the root module or between modules.

# Reference outputs from the web-server module
output "web_instance_ids" {
  value = module.web_servers.instance_ids
}

output "web_private_ips" {
  value = module.web_servers.instance_private_ips
}

# Pass outputs between modules
module "load_balancer" {
  source = "./modules/load-balancer"
  
  name               = "web"
  vpc_id             = module.vpc.vpc_id
  subnet_ids         = module.vpc.public_subnets
  target_instance_ids = module.web_servers.instance_ids
  security_group_id  = module.web_servers.security_group_id
}

Module Versioning with Git

Versioning your modules ensures stability and allows controlled updates.

1. Store Modules in Git Repositories

Create separate repositories for your modules:

# Module repository structure
terraform-aws-web-server/
├── main.tf
├── variables.tf
├── outputs.tf
├── README.md
└── versions.tf

2. Tag Releases with Semantic Versioning

git tag v1.0.0
git tag v1.1.0
git tag v2.0.0
git push --tags

3. Reference Versioned Modules

module "web_server" {
  source = "git::https://github.com/my-org/terraform-aws-web-server.git?ref=v1.2.0"
  
  name           = "production"
  instance_type = "t3.large"
  instance_count = 3
  vpc_id         = module.vpc.vpc_id
  subnet_ids     = module.vpc.private_subnets
}

Versioning Best Practices

  • Use semantic versioning: MAJOR.MINOR.PATCH
  • Tag releases: Create tags for stable versions
  • Pin versions: Always specify version constraints
  • Test upgrades: Test minor and patch versions before applying
  • Maintain changelog: Document changes between versions

Complete Module Example

Here's a complete example of a reusable web server cluster module:

# modules/web-cluster/variables.tf
variable "name" { type = string }
variable "environment" { type = string }
variable "instance_type" { type = string; default = "t3.micro" }
variable "min_size" { type = number; default = 1 }
variable "max_size" { type = number; default = 3 }
variable "vpc_id" { type = string }
variable "subnet_ids" { type = list(string) }

# modules/web-cluster/main.tf
resource "aws_launch_template" "web" {
  name_prefix   = "${var.name}-${var.environment}-"
  image_id      = "ami-0c02fb55956c7d316"
  instance_type = var.instance_type
  vpc_security_group_ids = [aws_security_group.web.id]
  
  user_data = base64encode(file("${path.module}/user-data.sh"))
}

resource "aws_autoscaling_group" "web" {
  name               = "${var.name}-${var.environment}-asg"
  min_size           = var.min_size
  max_size           = var.max_size
  vpc_zone_identifier = var.subnet_ids
  
  launch_template {
    id      = aws_launch_template.web.id
    version = "$Latest"
  }
  
  tag {
    key                 = "Name"
    value               = "${var.name}-${var.environment}"
    propagate_at_launch = true
  }
}

# modules/web-cluster/outputs.tf
output "asg_name" {
  value = aws_autoscaling_group.web.name
}

output "security_group_id" {
  value = aws_security_group.web.id
}

Key Takeaways

  • Modules promote reusability: Write once, use across projects
  • Standardize infrastructure: Enforce best practices organization-wide
  • Abstract complexity: Hide implementation details behind clean interfaces
  • Version control modules: Use Git tags for stable releases
  • Compose complex systems: Build complex infrastructure from simple modules
  • Leverage public modules: Save time with community-tested patterns

In our next tutorial, we'll dive into Terraform Workspaces and Remote State, where you'll learn how to manage multiple environments and collaborate effectively with your team.


No comments:

Post a Comment

How to Create and Use Terraform Modules for Reusable Code

How to Create and Use Terraform Modules for Reusable Code Terraform...