Modules und Wiederverwendbarkeit
Terraform Modules strukturieren und teilen.
Lernziele
Nach dieser Lektion kannst du Terraform-Module schreiben, konsumieren und über die Registry beziehen. Du verstehst die Schnittstelle aus Variables, Outputs und der module-Block-Syntax. Du kennst Composition-Patterns für Plattform-Teams und weißt, wann ein Modul zu viel Abstraktion wird.
Das Problem: Copy-Paste skaliert nicht
Dein Team hat ein VPC-Modul in drei Projekten — per Copy-Paste. In Projekt A fehlt die Flow-Log-Konfiguration, die du letzte Woche in Projekt B ergänzt hast. Projekt C nutzt noch die alte Subnetz-Maskierung. Drei VPCs, drei leicht unterschiedliche Implementierungen, keiner merkt es, bis ein Audit-Tool Alarm schlägt.
Module kapseln wiederverwendbare Infrastruktur-Muster in einem Verzeichnis mit definierter Schnittstelle. Du änderst einmal, alle Consumer profitieren — vorausgesetzt, du versionierst diszipliniert.
Anatomie eines Moduls
Ein Modul ist schlicht ein Verzeichnis mit .tf-Dateien. Das Root-Modul ist dein Projekt-Root; alles darunter sind Child-Module:
platform/
├── main.tf # Root-Modul — ruft Module auf
├── variables.tf
├── outputs.tf
└── modules/
└── vpc/
├── main.tf
├── variables.tf
└── outputs.tf
Im Root-Modul rufst du das Child-Modul auf:
module "vpc" {
source = "./modules/vpc"
name = "platform-prod"
cidr = "10.0.0.0/16"
availability_zones = ["eu-central-1a", "eu-central-1b"]
enable_flow_logs = true
}
output "vpc_id" {
value = module.vpc.vpc_id
}
Im Modul selbst (modules/vpc/variables.tf):
variable "name" {
type = string
}
variable "cidr" {
type = string
}
variable "availability_zones" {
type = list(string)
}
variable "enable_flow_logs" {
type = bool
default = false
}
Und in modules/vpc/outputs.tf:
output "vpc_id" {
value = aws_vpc.main.id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
Die Regel: Inputs über Variables, Ergebnisse über Outputs. Was das Modul intern tut, ist dem Consumer egal — solange die Schnittstelle stabil bleibt.
Module Sources: lokal, Git, Registry
| Source | Beispiel | Wann |
|---|---|---|
| Lokal | source = "./modules/vpc" | Gleiches Repo, schnelle Iteration |
| Git | source = "git::https://github.com/org/tf-modules.git//vpc?ref=v2.1.0" | Geteilte Module über Repos |
| Registry | source = "terraform-aws-modules/vpc/aws" | Öffentliche oder private Registry |
Registry-Module haben oft tausende Downloads — terraform-aws-modules/vpc/aws ist der Klassiker. Lies trotzdem den Source: Black-Box-Module mit 80 undokumentierten Variables sind ein Anti-Pattern.
Versionierung bei Git/Registry:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "platform-prod"
cidr = "10.0.0.0/16"
azs = ["eu-central-1a", "eu-central-1b"]
}
Ohne version oder ref holst du bei jedem init den neuesten Stand — und wunderst dich über Breaking Changes.
Composition: Module schachteln
Plattform-Teams bauen oft eine Schichtung:
root (env/prod)
└── module "platform" (internes Meta-Modul)
├── module "vpc"
├── module "eks"
└── module "dns"
Das Root-Modul pro Umgebung ist dünn — es setzt nur Umgebungsvariablen und ruft das Meta-Modul auf. Die Komplexität steckt in den wiederverwendbaren Modulen, nicht in env/prod/main.tf.
Jedes Modul hat eigenen State-Namespace innerhalb des Root-States — du siehst im State Einträge wie module.vpc.aws_vpc.main. Module erzeugen keinen separaten State (außer du nutzt bewusst separate Root-Module pro Stack, Lektion 6).
Wann kein Modul — und wann zu viel Modul
Kein Modul: Ein One-Off für ein Experiment, das nie wiederholt wird. Die Abstraktionskosten übersteigen den Nutzen.
Zu viel Modul: Ein „Universal-VPC”-Modul mit 40 Boolean-Flags (enable_nat, enable_vpn, enable_flow_logs, single_nat, …). Niemand versteht die Kombinatorik, und jeder Bugfix bricht drei Consumer. Besser: kleine, fokussierte Module (vpc-standard, vpc-peering) statt eines God-Moduls.
Faustregel: Wenn du dasselbe HCL-Pattern zum dritten Mal schreibst, wird es ein Modul. Wenn das Modul mehr als ~10 Inputs braucht, ist es wahrscheinlich zu groß.
Praxis: Modul lokal bauen und testen
mkdir -p modules-lab/modules/label && cd modules-lab
cat > modules/label/main.tf <<'EOF'
variable "prefix" { type = string }
variable "name" { type = string }
locals {
id = "${var.prefix}-${var.name}"
}
output "id" {
value = local.id
}
EOF
cat > main.tf <<'EOF'
module "app" {
source = "./modules/label"
prefix = "platprep"
name = "api"
}
module "db" {
source = "./modules/label"
prefix = "platprep"
name = "postgres"
}
output "app_id" { value = module.app.id }
output "db_id" { value = module.db.id }
EOF
terraform init
terraform apply -auto-approve
terraform state list # module.app..., module.db...
terraform destroy
Zwei Instanzen desselben Moduls, ein State — so funktioniert Wiederverwendung ohne Duplikation.
Typische Stolperfallen
Relative Pfade nach Verschieben kaputt: source = "./modules/vpc" ist relativ zum Modul, das den Block enthält — nicht zum cwd. Nach Refactoring plötzlich „Module not found”.
Outputs vergessen: Consumer können module.vpc.vpc_id nicht nutzen, wenn das Modul keinen Output exportiert. Outputs sind Teil der API — dokumentiere sie.
Registry-Modul blind vertrauen: Major-Upgrades ohne Changelog-Lektüre. Pinne Versionen und teste Upgrades in Staging.
Zirkuläre Modul-Abhängigkeiten: Modul A ruft Modul B, B ruft A — Terraform bricht ab. Abhängigkeiten müssen azyklisch sein; teile gemeinsame Logik in ein drittes Modul.
Interview-Vorbereitung
„Was ist ein Terraform-Modul?” — Ein Container für Ressourcen mit definierter Schnittstelle (Variables rein, Outputs raus). Wird über module-Block mit source eingebunden.
Follow-ups:
- „Unterschied Root-Modul vs. Child-Modul?” — Root ist der Einstiegspunkt mit Backend; Child-Module werden aufgerufen und teilen desselben State.
- „Wie versionierst du Module?” — Git-Tags (
?ref=v1.2.0) oder Registry-Version-Constraints. - „Wann würdest du kein Modul bauen?” — Einmalige Ressourcen, oder wenn Abstraktion die Lesbarkeit verschlechtert.
- „Wie greifst du auf Modul-Outputs zu?” —
module.<name>.<output>.
Zusammenfassung
Module kapseln wiederholbare Infrastruktur-Patterns mit klarer Schnittstelle aus Variables und Outputs. Du beziehst sie lokal, aus Git oder der Registry — immer mit gepinnter Version. Plattform-Teams schichten Module (VPC → EKS → Apps) und halten Root-Module pro Umgebung schlank. Zu viel Abstraktion ist genauso schädlich wie Copy-Paste — fokussierte Module mit wenigen, gut dokumentierten Inputs gewinnen.
In der nächsten Lektion klären wir, wie du dev, staging und prod trennst — mit Workspaces oder Verzeichnisstruktur.