Proiectarea modulelor Terraform reutilizabile: Structură, I/O și Registry
Când începi să lucrezi cu Terraform pe proiecte reale, vine timpul de criză la 500-600 de linii de HCL: configurația începe să devină dificil de citit, aceleași modele se repetă pentru medii diferite, iar refactorizarea devine o operațiune riscant. Soluția este i Module Terraform: unități de abstractizare reutilizabile care încapsulează complexitatea și expun o interfață curată.
Acest ghid acoperă proiectarea formularelor profesionale: nu doar modul de creare a acestora, dar cum gandeste-te la interfața publică, gestionați compatibilitatea inversă, testați și publicați în Registrul Terraform. Un modul prost proiectat și mai rău decât niciun modul: creează dependențe rigide, îngreunează upgrade-urile și ascunde complexitatea în loc să o gestioneze.
Ce vei învăța
- Structura canonică a unui modul profesional Terraform
- Design variabil: tipuri, validări, valori implicite și obiecte complexe
- Ieșiri standardizate: ce să afișați și cu ce convenție de denumire
- Module copil și module compuse (compunere vs moștenire)
- Versiune semantică și gestionarea compatibilității cu versiuni inverse
- Publicare pe Registrul Public Terraform și pe un registru privat
- Modele avansate: forme generice, dinamice for_each, forme condiționale
Structura canonică a unui modul
Structura unui modul Terraform urmează convenții bine definite pe care Registrul și comunitate recunoaște. Abaterea de la aceste convenții creează confuzie pentru consumatorii formei.
terraform-aws-networking/ # Naming: terraform-{provider}-{name}
├── main.tf # Logica principale del modulo
├── variables.tf # Input variables (interfaccia pubblica)
├── outputs.tf # Output values (interfaccia pubblica)
├── versions.tf # required_providers e terraform version
├── locals.tf # Valori computati interni
├── README.md # Documentazione (auto-generabile con terraform-docs)
├── CHANGELOG.md # Versioni e breaking changes
├── LICENSE # Apache 2.0 per moduli pubblici
├── examples/ # Esempi di utilizzo del modulo
│ ├── simple/
│ │ ├── main.tf # Uso minimo del modulo
│ │ └── outputs.tf
│ └── complete/
│ ├── main.tf # Uso completo con tutti i parametri
│ ├── variables.tf
│ └── outputs.tf
├── modules/ # Submoduli interni (opzionale)
│ └── subnet/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── test/ # Test del modulo
└── networking_test.go # Terratest
Convenția de numire terraform-{provider}-{name} si obligatoriu pentru
publicație în Registrul public Terraform și recomandat pentru registrele private.
Exemple reale: terraform-aws-eks, terraform-google-kubernetes-engine,
terraform-azurerm-network.
Design variabil: interfața publică
Variabilele unui modul sunt API-ul său public. Design variabil bun urmează pe principiul uimrii minime: un consumator al modulului ar trebui să poată înțelege ce să facă citind doar numele și descrierile variabilelor, fără a citi codul intern.
# variables.tf — design professionale delle variabili
# Variabile richiesta (no default): rappresenta un input fondamentale
variable "vpc_cidr" {
description = "CIDR block per la VPC. Deve essere un CIDR /16 privato."
type = string
validation {
condition = can(regex(
"^(10\\.|172\\.(1[6-9]|2[0-9]|3[0-1])\\.|192\\.168\\.)\\d+\\.\\d+/16$",
var.vpc_cidr
))
error_message = "Il CIDR deve essere un blocco privato /16 (RFC 1918)."
}
}
# Variabile con default ragionevole
variable "name" {
description = "Nome base usato per il prefisso di tutte le risorse."
type = string
default = "main"
validation {
condition = can(regex("^[a-z][a-z0-9-]{1,28}[a-z0-9]$", var.name))
error_message = "Il nome deve essere lowercase, alfanumerico con trattini, 3-30 caratteri."
}
}
# Oggetto complesso: meglio di N variabili separate per configurazioni correlate
variable "nat_gateway_config" {
description = <<-EOT
Configurazione del NAT Gateway.
- enabled: crea il NAT Gateway (aggiunge costo ~$32/mese per AZ)
- single_az: usa un solo NAT Gateway (risparmio costi per non-prod)
EOT
type = object({
enabled = bool
single_az = bool
})
default = {
enabled = false
single_az = false
}
}
# Lista di oggetti: per configurazioni ripetute
variable "private_subnets" {
description = "Lista di subnet private da creare."
type = list(object({
cidr = string
availability_zone = string
tags = optional(map(string), {})
}))
default = []
validation {
condition = alltrue([
for s in var.private_subnets :
can(cidrhost(s.cidr, 0))
])
error_message = "Ogni subnet deve avere un CIDR valido."
}
}
# Variabile sensibile: non viene loggata nell'output di plan/apply
variable "database_password" {
description = "Password per il database. Usa Secrets Manager in produzione."
type = string
sensitive = true
validation {
condition = length(var.database_password) >= 16
error_message = "La password deve avere almeno 16 caratteri."
}
}
# Map per tag: pattern universale in Terraform
variable "tags" {
description = "Map di tag aggiuntivi da applicare a tutte le risorse."
type = map(string)
default = {}
}
# Feature flags: booleani che attivano/disattivano funzionalita
variable "enable_flow_logs" {
description = "Abilita VPC Flow Logs per il network monitoring."
type = bool
default = false
}
variable "enable_vpc_endpoints" {
description = "Crea VPC Endpoints per S3 e DynamoDB (riduce costi NAT)."
type = bool
default = true
}
Logica internă: localnici și proiectarea resurselor
În interiorul modulului, i locals sunt instrumentul principal de ținere
codul DRY. Toată logica de calcul trebuie să fie în local, nu dispersată
în blocuri de resurse.
# locals.tf — logica interna del modulo
locals {
# Naming convention centralizzata
name_prefix = var.name
# Merging tag: tag del modulo + tag dell'utente
# I tag dell'utente sovrascrivono i default del modulo
default_tags = {
ManagedBy = "Terraform"
Module = "terraform-aws-networking"
}
merged_tags = merge(local.default_tags, var.tags)
# Calcola automaticamente le AZ disponibili se non specificate
azs = length(var.private_subnets) > 0 ? [
for s in var.private_subnets : s.availability_zone
] : []
# Decisione sul NAT Gateway: uno per AZ o uno singolo
nat_count = var.nat_gateway_config.enabled ? (
var.nat_gateway_config.single_az ? 1 : length(local.azs)
) : 0
# Mappa per for_each: key univoca -> oggetto configurazione
private_subnets_map = {
for idx, subnet in var.private_subnets :
"${local.name_prefix}-private-${idx + 1}" => subnet
}
}
# main.tf — risorse del modulo
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(local.merged_tags, {
Name = "${local.name_prefix}-vpc"
})
}
# for_each su mappa: crea una risorsa per ogni elemento
resource "aws_subnet" "private" {
for_each = local.private_subnets_map
vpc_id = aws_vpc.this.id
cidr_block = each.value.cidr
availability_zone = each.value.availability_zone
tags = merge(
local.merged_tags,
each.value.tags,
{
Name = each.key
Tier = "Private"
}
)
}
# Risorsa condizionale: created solo se nat_gateway_config.enabled = true
resource "aws_eip" "nat" {
count = local.nat_count
domain = "vpc"
tags = merge(local.merged_tags, {
Name = "${local.name_prefix}-nat-eip-${count.index + 1}"
})
}
resource "aws_nat_gateway" "this" {
count = local.nat_count
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
depends_on = [aws_internet_gateway.this]
tags = merge(local.merged_tags, {
Name = "${local.name_prefix}-nat-${count.index + 1}"
})
}
# Flow Logs condizionali
resource "aws_cloudwatch_log_group" "vpc_flow_logs" {
count = var.enable_flow_logs ? 1 : 0
name = "/aws/vpc-flow-logs/${aws_vpc.this.id}"
retention_in_days = 30
tags = local.merged_tags
}
resource "aws_flow_log" "this" {
count = var.enable_flow_logs ? 1 : 0
vpc_id = aws_vpc.this.id
traffic_type = "ALL"
iam_role_arn = aws_iam_role.flow_logs[0].arn
log_destination = aws_cloudwatch_log_group.vpc_flow_logs[0].arn
}
Rezultat: Ce să expuneți și cum
Ieșirile unui modul sunt la fel de importante ca și variabilele: sunt interfața pe care modulele părinte și alte module le folosesc pentru a obține informații despre resursele create. Regula de aur este: expune tot ce ar putea avea nevoie un consumator, dar nu mai mult.
# outputs.tf — output standardizzati del modulo
# ID della VPC: sempre necessario per altri moduli
output "vpc_id" {
description = "ID della VPC creata."
value = aws_vpc.this.id
}
output "vpc_cidr" {
description = "CIDR block della VPC."
value = aws_vpc.this.cidr_block
}
# Liste di IDs: convenzione piu comune per subnet
output "private_subnet_ids" {
description = "Lista degli IDs delle subnet private."
value = [for s in aws_subnet.private : s.id]
}
# Map key->id: utile quando il consumatore deve referenziare subnet per nome
output "private_subnet_ids_by_name" {
description = "Map: nome subnet -> ID. Utile per for_each in moduli parent."
value = { for k, v in aws_subnet.private : k => v.id }
}
# ARN per policy IAM
output "vpc_arn" {
description = "ARN della VPC."
value = aws_vpc.this.arn
}
# Output condizionale: null se la risorsa non e stata creata
output "nat_gateway_ids" {
description = "IDs dei NAT Gateway. Lista vuota se nat_gateway_config.enabled = false."
value = aws_nat_gateway.this[*].id
}
# Output di un oggetto completo: utile per passare configurazione a moduli figli
output "vpc_config" {
description = "Configurazione completa della VPC per uso in moduli downstream."
value = {
id = aws_vpc.this.id
arn = aws_vpc.this.arn
cidr_block = aws_vpc.this.cidr_block
private_subnet_ids = [for s in aws_subnet.private : s.id]
nat_gateway_enabled = var.nat_gateway_config.enabled
}
}
Utilizarea unui modul: Sintaxă și Versiune
Când utilizați un modul, Terraform acceptă diferite surse: local, Git, Terraform Registry. Gestionarea versiunilor este esențială pentru a evita regresiile neașteptate.
# Uso del modulo da sorgenti diverse
# 1. Modulo locale (sviluppo e test)
module "networking" {
source = "./modules/networking"
vpc_cidr = "10.0.0.0/16"
name = "dev"
}
# 2. Dal Terraform Registry pubblico con versione pinned
module "networking" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.1" # Permette 5.1.x ma non 5.2.0
name = "dev-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 # Per dev/staging: risparmio costi
tags = {
Environment = "dev"
ManagedBy = "Terraform"
}
}
# 3. Da repository Git (registry privato aziendale)
module "networking" {
source = "git::https://github.com/myorg/terraform-modules.git//networking?ref=v2.3.1"
vpc_cidr = "10.0.0.0/16"
name = "prod"
nat_gateway_config = {
enabled = true
single_az = false # Multi-AZ in produzione
}
}
# 4. Da Terraform Enterprise o HCP Terraform Registry privato
module "networking" {
source = "app.terraform.io/myorg/networking/aws"
version = "~> 2.0"
vpc_cidr = "10.0.0.0/16"
}
# Accesso agli output del modulo
resource "aws_eks_cluster" "main" {
name = "prod-cluster"
role_arn = aws_iam_role.eks.arn
vpc_config {
# Referenzia l'output del modulo networking
subnet_ids = module.networking.private_subnet_ids
}
}
output "vpc_id" {
value = module.networking.vpc_id
}
Versiune semantică pentru module
Modulele Terraform urmează versiune semantică (mai multe):
MAJOR.MINOR.PATCH. Distincția dintre ruperea schimbării și non-ruperea
schimbare și critică pentru cei care consumă modulul.
# Cosa costituisce un breaking change (bump MAJOR):
# - Rimozione di una variabile obbligatoria
# - Cambiamento del tipo di una variabile esistente
# - Rimozione di un output
# - Rinomina di un output
# - Cambiamento del nome di una risorsa (causa destroy + recreate)
# - Aggiunta di una variabile obbligatoria senza default
# Non-breaking change (bump MINOR):
# - Aggiunta di una variabile opzionale (con default)
# - Aggiunta di un nuovo output
# - Aggiunta di una nuova funzionalita opzionale (feature flag)
# Patch:
# - Bugfix che non cambia l'interfaccia
# - Aggiornamento di versione di un sub-modulo
# - Miglioramenti alla documentazione
# Convenzioni nei version constraints:
# ~> 2.0 = >= 2.0, < 3.0 (piu usato: permette minor e patch)
# ~> 2.3 = >= 2.3, < 3.0
# ~> 2.3.0 = >= 2.3.0, < 2.4.0 (solo patch)
# >= 2.0, < 3.0 (equivalente a ~> 2.0 ma piu esplicito)
# CHANGELOG.md esempio:
# ## [3.0.0] - 2026-08-01
# ### Breaking Changes
# - Rimossa variabile `legacy_dns_mode` (deprecata dalla v2.5)
# - Output `subnet_id` rinominato in `private_subnet_ids` (lista)
#
# ## [2.5.0] - 2026-07-15
# ### Added
# - Aggiunto supporto VPC Endpoints per S3 e DynamoDB
# - Nuova variabile opzionale `enable_vpc_endpoints` (default: false)
# ### Deprecated
# - Variabile `legacy_dns_mode` sara rimossa nella v3.0
Modele avansate: module generice
Cele mai puternice module sunt cele pe care le folosesc for_each dinamic de creat
configurații complet parametrice. Acest model este comun în modulele pentru
gestionați seturi de resurse similare (de exemplu, mai multe subrețele, mai multe reguli pentru grupuri de securitate).
# Pattern: modulo per Security Groups completamente configurabile
# variables.tf del modulo sg
variable "security_groups" {
description = "Map di security groups da creare."
type = map(object({
description = string
ingress_rules = optional(list(object({
description = string
from_port = number
to_port = number
protocol = string
cidr_blocks = optional(list(string), [])
security_group_ids = optional(list(string), [])
})), [])
egress_rules = optional(list(object({
description = string
from_port = number
to_port = number
protocol = string
cidr_blocks = optional(list(string), ["0.0.0.0/0"])
})), [])
tags = optional(map(string), {})
}))
default = {}
}
# main.tf del modulo sg
resource "aws_security_group" "this" {
for_each = var.security_groups
name = "${var.name_prefix}-${each.key}"
description = each.value.description
vpc_id = var.vpc_id
tags = merge(var.tags, each.value.tags, {
Name = "${var.name_prefix}-${each.key}"
})
lifecycle {
create_before_destroy = true
}
}
# Flatten per le ingress rules: ogni SG puo avere N regole
locals {
ingress_rules = flatten([
for sg_name, sg_config in var.security_groups : [
for idx, rule in sg_config.ingress_rules : {
sg_name = sg_name
rule_idx = idx
rule = rule
}
]
])
}
resource "aws_security_group_rule" "ingress" {
for_each = {
for item in local.ingress_rules :
"${item.sg_name}-ingress-${item.rule_idx}" => item
}
type = "ingress"
security_group_id = aws_security_group.this[each.value.sg_name].id
description = each.value.rule.description
from_port = each.value.rule.from_port
to_port = each.value.rule.to_port
protocol = each.value.rule.protocol
cidr_blocks = each.value.rule.cidr_blocks
}
# Uso del modulo sg:
module "security_groups" {
source = "./modules/security-groups"
name_prefix = local.name_prefix
vpc_id = module.networking.vpc_id
security_groups = {
"web" = {
description = "Security group per istanze web"
ingress_rules = [
{
description = "HTTPS dal pubblico"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
{
description = "HTTP redirect dal pubblico"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
]
}
"database" = {
description = "Security group per RDS"
ingress_rules = [
{
description = "PostgreSQL solo dal tier web"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_group_ids = [module.security_groups.ids["web"]]
cidr_blocks = []
}
]
}
}
}
Documentare automată cu terraform-docs
Documentarea manuală a variabilelor și ieșirilor unui modul este plictisitoare și adesea depășită. terraform-docs generează automat documentația din codul HCL.
# Installa terraform-docs
brew install terraform-docs # macOS
# oppure
go install github.com/terraform-docs/terraform-docs@latest
# Genera README.md dal modulo
terraform-docs markdown table . > README.md
# Configurazione in .terraform-docs.yml
# formatter: "markdown table"
# output:
# file: README.md
# mode: inject # Inietta tra i marker nel README esistente
# settings:
# show-all: true
# indent: 2
# Con inject mode nel README.md:
#
# (documentazione auto-generata)
#
# Aggiorna il README automaticamente con pre-commit
# .pre-commit-config.yaml:
# - repo: https://github.com/terraform-docs/terraform-docs
# rev: "v0.19.0"
# hooks:
# - id: terraform-docs-go
# args: ["--output-file", "README.md", "--output-mode", "inject", "."]
Publicați în Registrul Terraform
Pentru a publica un modul în Registrul public Terraform, depozitul GitHub trebuie respectă convențiile specifice. Procesul este aproape complet automatizat prin GitHub Tags.
# Pre-requisiti per la pubblicazione:
# 1. Repository pubblico su GitHub
# 2. Nome repository: terraform-{provider}-{name}
# 3. Tag semver nel formato: v{MAJOR}.{MINOR}.{PATCH}
# Struttura obbligatoria per il Registry:
# main.tf, variables.tf, outputs.tf nella root
# README.md con documentazione
# Almeno un esempio in examples/
# Processo di release:
git tag -a v1.0.0 -m "Release v1.0.0: versione iniziale"
git push origin v1.0.0
# Il Registry riceve una webhook e indicizza automaticamente il modulo
# Per registry privato con HCP Terraform:
# 1. Vai su app.terraform.io -> Registry -> Publish Module
# 2. Connetti il repository GitHub
# 3. I tag vengono sincronizzati automaticamente
# terraform.tfvars per testing locale del modulo
vpc_cidr = "10.0.0.0/16"
name = "test"
nat_gateway_config = {
enabled = false
single_az = false
}
tags = {
Environment = "test"
Owner = "platform-team"
}
Module open source recomandate pentru AWS
Înainte de a scrie un modul de la zero, verificați dacă există deja o versiune matură în Registry. Modulele de terraform-aws-module (de Anton Babenko) sunt standardul de facto:
terraform-aws-modules/vpc/aws— rețea completăterraform-aws-modules/eks/aws— cluster EKSterraform-aws-modules/rds/aws— RDS cu toți parametriiterraform-aws-modules/s3-bucket/aws— S3 cu criptare și politiciterraform-aws-modules/security-group/aws— Peste 100 de reguli preconfigurate
Concluzii și pașii următori
Proiectarea unor module Terraform bune necesită aceeași grijă ca și proiectarea unui API bun: gândiți-vă la consumatorii dvs., mențineți interfața stabilă, documentați fiecare parametru. Un modul bine conceput este folosit de întreaga echipă ani de zile fără modificări.
Următorul pas în maturitatea dvs. Terraform este să vă adresați managementului de stat în echipe: cea mai frecventă și riscantă problemă în scalarea organizațiilor adoptând Terraform.
Seria completă: Terraform și IaC
- Articolul 01 — Terraform de la zero: HCL, Provider și Plan-Apply-Destroy
- Articolul 02 (acest) — Proiectarea modulelor Terraform reutilizabile: Structură, I/O și Registry
- Articolul 03 — Stare Terraform: Backend la distanță cu S3/GCS, Blocare și Import
- Articolul 04 — Terraform în CI/CD: Flux de lucru GitHub Actions, Atlantis și Pull Request
- Articolul 05 — Testarea IaC: Terratest, Terraform Native Test și Contract Testing
- Articolul 06 — Securitatea IaC: Politica Checkov, Trivy și OPA ca cod
- Articolul 07 — Terraform Multi-Cloud: AWS + Azure + GCP cu module partajate
- Articolul 08 — GitOps pentru Terraform: Controler Flux TF, Spacelift și Detecție Drift
- Articolul 09 – Terraform vs Pulumi vs OpenTofu: Comparație finală 2026
- Articolul 10 — Modele Terraform Enterprise: Spațiu de lucru, Sentinel și scalarea echipei







