📦 Modules in Terraform: Reusable and Scalable Architecture
A Terraform Module is a set of .tf configuration files packaged in a single folder to define a reusable infrastructure component (for example: a full VPC network, a Kubernetes EKS cluster, or a PostgreSQL database with replicas).
1. What is a Module and existing types?
In Terraform, any directory that contains .tf files is considered a module. There are two main classifications:
- Root Module: The main directory from which you run the
terraform applyorterraform plancommands. Contains global configuration and calls other modules.
- Child Module: A packaged child module that is called within the Root Module using a
module "nombre" {}block.
Programmatic Analogy: Think of a Module as a Function in traditional languages. It accepts input parameters (variables.tf), executes business logic internally (main.tf), and returns a result (outputs.tf).
2. Recommended File Structure
To follow community and HashiCorp conventions, the internal structure of any module must be clean and predictable:
modules/aws-vpc/
├── README.md # Documentación de uso y parámetros
├── main.tf # Recursos principales (VPC, Subnets, Gateways)
├── variables.tf # Variables de entrada expuestas al usuario
├── outputs.tf # Valores retornados (IDs de subnets, VPC ID, etc.)
├── versions.tf # Versión mínima de Terraform y proveedores
└── examples/ # Ejemplos de implementación listos para usar
└── basic-vpc/
└── main.tf3. Creating your First Reusable Module (Child Module)
Let's design a basic module to provision an EC2 web server with its corresponding Security Group:
variable "instance_type" {
type = string
default = "t3.micro"
description = "Tipo de instancia EC2"
}
variable "server_name" {
type = string
description = "Nombre identificador del servidor"
}
variable "vpc_id" {
type = string
description = "ID de la VPC donde residirá el servidor"
}resource "aws_security_group" "web_sg" {
name = "${var.server_name}-sg"
description = "Allow HTTP traffic"
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0" # Amazon Linux 2023
instance_type = var.instance_type
vpc_security_group_ids = [aws_security_group.web_sg.id]
tags = {
Name = var.server_name
}
}output "instance_id" {
value = aws_instance.web.id
description = "ID de la instancia EC2 creada"
}
output "public_ip" {
value = aws_instance.web.public_ip
description = "Dirección IP pública del servidor"
}4. Invocation of Modules from the Root Module
Once the module is created, it is invoked from the main main.tf file by passing the required parameters through the module block:
module "frontend_server" {
source = "./modules/web-server"
server_name = "mc-frontend-prod"
instance_type = "t3.small"
vpc_id = "vpc-0a1b2c3d4e5f"
}
# Referenciando la salida (output) del módulo en otro recurso
output "app_url" {
value = "http://${module.frontend_server.public_ip}"
}5. Module Sources
The source directive tells Terraform where to download the module code from:
- Relative Local Route:
source = "./modules/vpc"
- Private or Public Git Repository:
source = "git::https://example.com/storage.git?ref=v1.2.0"
- Public Terraform Registry:
source = "terraform-aws-modules/vpc/aws"
- Private Registry (HCP / Terraform Cloud):
source = "app.terraform.io/mi-org/vpc/aws"
6. Good Versioning Practices
When modules are shared across multiple teams or projects, it is critical to use strict versioning to not break infrastructures in production if the module definition changes:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.8.1" # Fijar siempre la versión exacta en producción
name = "mc-production-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.101.0/24", "10.0.102.0/24"]
}7. Golden Rules in Module Construction
- Single Responsibility Principle: A module must do only one thing well (e.g. only manage the database or only the network, not everything together).
- Avoid Hardcoding Values: Do not place regions, subnet IDs or fixed names within the module code. State them as
variables.tf.
- Document Outputs: Expose primary identifiers (IDs, ARNs, endpoints) using
outputs.tfto allow composition with other modules.