Skip to main content

Terraform Modules


Chapter objectives

  • Understand modules
  • Create reusable modules
  • Use the Terraform Registry
  • Organize a project with modules

1 - What is a module?

Definition

A module is a container for several resources used together.

Advantages

Without modulesWith modules
Duplicated codeReusable code
Huge filesIsolated components
Hard to maintainEasy to test
No versioningVersionable

2 - Structure of a module

Standard tree

modules/
└── vpc/
├── main.tf # Ressources principales
├── variables.tf # Variables d'entrée
├── outputs.tf # Valeurs de sortie
├── versions.tf # Versions requises
└── README.md # Documentation

Example VPC module

# modules/vpc/variables.tf
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
}

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

variable "availability_zones" {
description = "List of availability zones"
type = list(string)
}
# modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true

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

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 = {
Name = "${var.environment}-public-${count.index + 1}"
}
}

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

tags = {
Name = "${var.environment}-igw"
}
}

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 = {
Name = "${var.environment}-public-rt"
}
}

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
}
# modules/vpc/outputs.tf
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}

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

output "vpc_cidr" {
description = "CIDR block of the VPC"
value = aws_vpc.main.cidr_block
}

3 - Use a module

Local module

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

vpc_cidr = "10.0.0.0/16"
environment = "production"
availability_zones = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
}

# Utiliser les outputs du module
resource "aws_instance" "web" {
subnet_id = module.vpc.public_subnet_ids[0]
# ...
}

# Re-exporter les outputs
output "vpc_id" {
value = module.vpc.vpc_id
}

Module from the Registry

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

name = "my-vpc"
cidr = "10.0.0.0/16"

azs = ["eu-west-1a", "eu-west-1b", "eu-west-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 = {
Environment = "production"
}
}

Module from Git

# GitHub
module "vpc" {
source = "github.com/myorg/terraform-modules//vpc?ref=v1.0.0"
}

# GitLab
module "vpc" {
source = "git::https://gitlab.com/myorg/terraform-modules.git//vpc?ref=v1.0.0"
}

# SSH
module "vpc" {
source = "[email protected]:myorg/terraform-modules.git//vpc?ref=v1.0.0"
}

4 - Module sources

Types of sources

Syntax

# Local
source = "./modules/vpc"
source = "../shared-modules/vpc"

# Terraform Registry
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"

# GitHub
source = "github.com/hashicorp/example"
source = "github.com/hashicorp/example//subdir"
source = "github.com/hashicorp/example?ref=v1.2.0"

# S3
source = "s3::https://s3-eu-west-1.amazonaws.com/bucket/modules/vpc.zip"

# HTTP
source = "https://example.com/modules/vpc.zip"

5 - Module versioning

Registry versions

module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0" # Exact
version = "~> 5.0" # >= 5.0.0, < 6.0.0
version = ">= 5.0, < 6.0" # Range
}

Git refs

# Tag
source = "github.com/org/repo?ref=v1.0.0"

# Branch
source = "github.com/org/repo?ref=develop"

# Commit
source = "github.com/org/repo?ref=abc1234"

6 - Nested modules

Composition

Pass outputs between modules

# root/main.tf

module "vpc" {
source = "./modules/vpc"

vpc_cidr = var.vpc_cidr
environment = var.environment
}

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

vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.public_subnet_ids
instance_count = var.instance_count
}

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

vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
allowed_sg_ids = [module.ec2.security_group_id]
}

7 - Module count and for_each

count

module "web_cluster" {
source = "./modules/ec2-cluster"
count = var.create_cluster ? 1 : 0

instance_count = 3
instance_type = "t3.micro"
}

# Accès
output "cluster_ips" {
value = var.create_cluster ? module.web_cluster[0].instance_ips : []
}

for_each

variable "environments" {
default = {
dev = {
instance_type = "t3.micro"
instance_count = 1
}
staging = {
instance_type = "t3.small"
instance_count = 2
}
production = {
instance_type = "t3.large"
instance_count = 3
}
}
}

module "env" {
source = "./modules/environment"
for_each = var.environments

environment = each.key
instance_type = each.value.instance_type
instance_count = each.value.instance_count
}

# Accès
output "env_vpc_ids" {
value = { for k, v in module.env : k => v.vpc_id }
}

8 - Terraform Registry

ProviderModuleDescription
AWSterraform-aws-modules/vpc/awsComplete VPC
AWSterraform-aws-modules/eks/awsEKS cluster
AWSterraform-aws-modules/rds/awsRDS database
AzureAzure/network/azurermVirtual Network
GCPterraform-google-modules/network/googleVPC Network

EKS example

module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "19.0.0"

cluster_name = "my-cluster"
cluster_version = "1.28"

vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets

eks_managed_node_groups = {
default = {
min_size = 1
max_size = 3
desired_size = 2

instance_types = ["t3.medium"]
}
}

tags = {
Environment = "production"
}
}

9 - Publishing a module

Structure for the Registry

terraform-aws-my-module/
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
├── README.md
├── LICENSE
├── examples/
│ ├── simple/
│ │ ├── main.tf
│ │ └── README.md
│ └── complete/
│ ├── main.tf
│ └── README.md
└── modules/
└── submodule/
├── main.tf
├── variables.tf
└── outputs.tf

Requirements

✅ Repo public sur GitHub
✅ Nommé terraform-<PROVIDER>-<NAME>
✅ README.md avec description
✅ LICENSE
✅ Tags Git pour versioning (v1.0.0)
✅ Structure standard

10 - Best practices

Structure

# ✅ Fichiers séparés
# main.tf - Ressources
# variables.tf - Variables d'entrée
# outputs.tf - Valeurs de sortie
# versions.tf - Contraintes de version

Variables

# ✅ Description et type pour toutes les variables
variable "instance_type" {
description = "Type d'instance EC2"
type = string
default = "t2.micro"
}

# ✅ Validation
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}

Documentation

# Module VPC

Creates a VPC with public and private subnets.

## Usage

```hcl
module "vpc" {
source = "./modules/vpc"

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

Inputs

NameDescriptionTypeDefault
vpc_cidrCIDR block for VPCstring-
environmentEnvironment namestring-

Outputs

NameDescription
vpc_idID of the VPC

---

## Summary

```mermaid
mindmap
root((Modules))
Sources
Local
Registry
Git
Structure
main.tf
variables.tf
outputs.tf
Reuse
count
for_each
Registry
Public
Private
Key points
  • Modules encapsulate related resources
  • Use versions for stability
  • The Registry offers ready-to-use modules
  • Always document your modules
  • Test with examples

Practical exercises

  1. Create a VPC module with public and private subnets
  2. Use a module from the Registry (terraform-aws-modules/vpc/aws)
  3. Create a module that calls other modules
  4. Publish a module to a Git repo

← State Management | Best practices →