Skip to content

Terraform Modules: Reusable Infrastructure Components

DodaTech 5 min read

Terraform modules are self-contained packages of Terraform configuration that manage a group of related infrastructure resources as a single reusable unit.

What You'll Learn

In this tutorial, you will learn how to create Terraform modules, use input variables and outputs to interface with modules, call modules from configurations, and publish modules for team reuse.

Why It Matters

Modules eliminate duplication and enforce consistency. Instead of copying resource blocks across environments, you define the pattern once and reuse it. Teams using modules report 60 percent faster infrastructure provisioning and fewer configuration errors.

Real-World Use

DodaTech maintains a library of Terraform modules for VPC, EC2, RDS, and load balancers. Durga Antivirus Pro's platform team composes these modules to spin up complete environments in minutes, with consistent security groups, tagging, and encryption settings.

Module Structure

A module is a directory containing Terraform files. It follows the same structure as a root module but is designed for reuse:

# modules/vpc/main.tf
resource "aws_vpc" "this" {
  cidr_block = var.cidr_block
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name        = var.name
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

resource "aws_subnet" "public" {
  count             = length(var.public_subnet_cidrs)
  vpc_id            = aws_vpc.this.id
  cidr_block        = var.public_subnet_cidrs[count.index]
  map_public_ip_on_launch = true
  availability_zone = var.availability_zones[count.index]
}
# modules/vpc/variables.tf
variable "name" {
  description = "Name prefix for VPC resources"
  type        = string
}

variable "environment" {
  description = "Deployment environment"
  type        = string
}

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

variable "public_subnet_cidrs" {
  description = "CIDR blocks for public subnets"
  type        = list(string)
}

variable "availability_zones" {
  description = "Availability zones for subnets"
  type        = list(string)
}
# modules/vpc/outputs.tf
output "vpc_id" {
  description = "The VPC ID"
  value       = aws_vpc.this.id
}

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

Calling a Module

Root configurations reference modules from the local filesystem, registry, or Git:

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

  name                = "dodatech-main"
  environment         = "production"
  cidr_block          = "10.0.0.0/16"
  public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
  availability_zones  = ["us-east-1a", "us-east-1b"]
}

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

  name        = "web-server"
  environment = "production"
  subnet_id   = module.vpc.public_subnet_ids[0]
  instance_type = "t3.medium"
}

Expected output: Terraform creates the VPC first (from the module), then creates the EC2 instance using the VPC's subnet IDs. Module outputs feed into other modules as inputs.

Module Registry

The Terraform Registry hosts thousands of public modules:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"

  name = "dodatech-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.3.0/24", "10.0.4.0/24"]

  enable_nat_gateway = true
  enable_vpn_gateway = false
  tags = {
    Environment = "production"
  }
}

Expected output: Running <a href="/devops/terraform/">terraform</a> init downloads the VPC module from the registry. The module creates a complete VPC with subnets, NAT gateway, and route tables.

Module Versioning

Pin module versions to prevent unexpected changes:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = ">= 5.0, < 6.0"

  name = "dodatech-vpc"
  cidr = "10.0.0.0/16"
}

Expected output: Terraform resolves the module version constraint at init time. The lock file records the exact version used.

Module Composition

Modules compose to build complete architectures:

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

module "database" {
  source = "./modules/rds"
  subnet_ids = module.network.private_subnet_ids
  # ... database inputs
}

module "application" {
  source = "./modules/ecs"
  vpc_id     = module.network.vpc_id
  db_address = module.database.endpoint
  # ... application inputs
}

Expected output: Three modules connected through outputs. Terraform builds a dependency graph and creates resources in the correct order: network, database, application.

Common Mistakes

1. Module Path Errors

Using a relative path that does not exist or forgetting to initialize modules with <a href="/devops/terraform/">terraform</a> init.

2. Not Pinning Module Versions

Calling source = "<a href="/devops/terraform/">terraform</a>-aws-modules/vpc/aws" without version constraints causes unpredictable upgrades.

3. Exposing Internal Resource Details

Outputting raw resource IDs creates tight coupling. Output only what consumers need.

4. Hardcoding Values Inside Modules

Modules should accept all configurable values through variables. Hardcoded values make modules unusable in different contexts.

5. Nesting Modules Too Deep

Deep module nesting (five-plus levels) makes debugging difficult. Prefer shallow, composable module structures.

Practice Questions

1. What is a Terraform module? A self-contained directory of Terraform configuration files that manages a group of related resources as a reusable unit.

2. How do you pass values into a module? Module input variables are set as arguments in the module block, for example module "vpc" { source = "./vpc" cidr = "10.0.0.0/16" }.

3. Why should you pin module versions? Version pinning prevents unexpected upgrades that could introduce breaking changes or alter infrastructure behavior.

4. What is the difference between a root module and a child module? Every Terraform configuration has a root module (the current working directory). Child modules are called from root or other modules using the module block.

5. Challenge: Create a module that provisions an EC2 instance with a security group. Call it from three different environments with different instance types and tag values.

Mini Project: Reusable Web Module

Create a web-server module that includes an EC2 instance, security group opening ports 80 and 443, and an Elastic IP. Call this module from root configurations for dev, staging, and production with different instance sizes.

Terraform State
Terraform Workspaces

What's Next

Build reusable Terraform modules to standardize your infrastructure patterns, then use Workspaces to manage multiple environments.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro