Terraform Modules Writing Reusable Infrastructure Code

Every experienced Terraform practitioner uses modules. They are the primary way to write infrastructure code that you can reuse across projects, environments, and teams without copying and pasting. This topic explains what modules are, how they work, and why they are the most important organisational concept in Terraform.

What Is a Module

A module is a directory that contains one or more Terraform .tf files. Every Terraform configuration you have written so far is technically a module — specifically, the root module. When you call another directory of Terraform code from your root module, that other directory becomes a child module.

Analogy: Functions in a Programming Language

In a programming language, you write a function once and call it as many times as you need — passing different inputs each time. Modules work exactly the same way. You write the infrastructure pattern once and call it with different inputs: once for development, once for staging, once for production.

The Problem Modules Solve

Without Modules: Repeated Code

project/
  dev/
    main.tf        # VPC + subnets + security groups
    variables.tf
  staging/
    main.tf        # Same code, different values
    variables.tf
  prod/
    main.tf        # Same code again, different values
    variables.tf

Three copies of the same code. A bug fix or improvement means updating all three files.

With Modules: One Source of Truth

project/
  modules/
    networking/
      main.tf        # Written once
      variables.tf
      outputs.tf
  dev/
    main.tf          # Just calls the module with dev values
  staging/
    main.tf          # Just calls the module with staging values
  prod/
    main.tf          # Just calls the module with prod values

Fix a bug in modules/networking/main.tf and all three environments benefit from the fix automatically on next apply.

Module Directory Structure

A well-structured module follows a consistent pattern:

modules/
  networking/
    main.tf        # Resources the module creates
    variables.tf   # Input variables (what callers must provide)
    outputs.tf     # Output values (what callers can read back)
    README.md      # Documentation (optional but recommended)

Writing a Simple Network Module

modules/networking/variables.tf

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

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

modules/networking/main.tf

resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr

  tags = {
    Name        = "${var.environment}-vpc"
    Environment = var.environment
  }
}

resource "aws_subnet" "public" {
  vpc_id     = aws_vpc.main.id
  cidr_block = cidrsubnet(var.vpc_cidr, 8, 1)

  tags = {
    Name = "${var.environment}-public-subnet"
  }
}

modules/networking/outputs.tf

output "vpc_id" {
  description = "ID of the created VPC"
  value       = aws_vpc.main.id
}

output "public_subnet_id" {
  description = "ID of the public subnet"
  value       = aws_subnet.public.id
}

Calling a Module

To use the module, add a module block in your root configuration and point its source at the module directory.

module "dev_network" {
  source = "./modules/networking"

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

module "prod_network" {
  source = "./modules/networking"

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

Two module calls. Two complete network setups. One module to maintain.

Diagram: Module Call Flow

Root module (main.tf)
      |
      |---> module "dev_network" { source = "./modules/networking" }
      |           |
      |           v
      |     modules/networking/
      |       - aws_vpc.main         (dev-vpc)
      |       - aws_subnet.public    (dev-public-subnet)
      |           |
      |           v
      |     Outputs:
      |       vpc_id = "vpc-0abc"
      |       public_subnet_id = "subnet-0def"
      |
      |---> module "prod_network" { source = "./modules/networking" }
                  |
                  v
            modules/networking/
              - aws_vpc.main         (prod-vpc)
              - aws_subnet.public    (prod-public-subnet)

Module Source Types

The source argument supports several source locations:

Source TypeExample
Local path"./modules/networking"
Terraform Registry"terraform-aws-modules/vpc/aws"
GitHub"github.com/org/repo//modules/network"
S3 bucket"s3::https://s3.amazonaws.com/bucket/module.zip"

Key Points

  • A module is any directory containing .tf files; the directory you run Terraform from is the root module.
  • Modules eliminate repeated code — write infrastructure patterns once, call them many times with different inputs.
  • A module uses variables.tf to accept inputs, main.tf to define resources, and outputs.tf to expose results.
  • Call a module with a module block, setting source to the module's path or registry address.
  • Run terraform init after adding or changing a module source to download or re-link it.

Leave a Comment