🏗️ Terraform Lektion 7/15 ~9 Min. Fortgeschritten

Modules und Wiederverwendbarkeit

Terraform Modules strukturieren und teilen.

📝 Meine Notizen

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

SourceBeispielWann
Lokalsource = "./modules/vpc"Gleiches Repo, schnelle Iteration
Gitsource = "git::https://github.com/org/tf-modules.git//vpc?ref=v2.1.0"Geteilte Module über Repos
Registrysource = "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.