Copy-Paste zwischen dev/, staging/ und prod/ funktioniert genau so lange, bis sich die drei Verzeichnisse unbemerkt auseinanderentwickeln. Module lösen das Problem — wenn sie richtig geschnitten sind.
Der richtige Schnitt
Ein gutes Modul kapselt eine fachliche Einheit, keine einzelne Ressource. Ein Modul namens aws_security_group, das nur eine Ressource durchreicht, bringt keinen Gewinn — es verdeckt nur das Original.
Sinnvoll sind Module wie:
network— VPC, Subnetze, Routing, NATkubernetes-cluster— Cluster, Node-Pools, zugehörige IAM-Rollenpostgres— Datenbank, Parametergruppe, Backups, Monitoring-Alarme
Faustregel: Wenn ein Modul mehr Variablen als Ressourcen hat, ist der Schnitt vermutlich falsch.
Klare Schnittstellen
Ein Modul wird über seine Variablen und Outputs benutzt — beide sind öffentliche API und sollten so behandelt werden. Typen und Beschreibungen sind Pflicht, validation verhindert Fehlbedienung früh:
variable "environment" {
type = string
description = "Zielumgebung, steuert Sizing und Backup-Retention."
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment muss dev, staging oder prod sein."
}
}
variable "node_count" {
type = number
description = "Anzahl der Worker-Nodes."
default = 3
}
Was ein Modul nach außen gibt, sollte ebenso bewusst gewählt sein: IDs und Endpunkte, die andere Module brauchen — nicht das komplette Ressourcen-Objekt.
Repo-Struktur
Bewährt hat sich die Trennung zwischen wiederverwendbaren Modulen und den Umgebungen, die sie zusammensetzen:
infrastructure/
├── modules/
│ ├── network/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── README.md
│ ├── kubernetes-cluster/
│ └── postgres/
└── environments/
├── dev/
│ ├── main.tf
│ ├── terraform.tfvars
│ └── backend.tf
├── staging/
└── prod/
Die Umgebungsverzeichnisse enthalten dann fast nur noch Verdrahtung:
module "network" {
source = "../../modules/network"
environment = "prod"
cidr_block = "10.20.0.0/16"
az_count = 3
}
module "cluster" {
source = "../../modules/kubernetes-cluster"
environment = "prod"
subnet_ids = module.network.private_subnet_ids
node_count = 6
}
Der Unterschied zwischen Staging und Produktion steht damit sichtbar in den tfvars — nicht versteckt in dupliziertem Code.
Module versionieren
Sobald mehrere Teams dieselben Module nutzen, gehören sie in ein eigenes Repository mit Git-Tags. Dann entscheidet jede Umgebung selbst, wann sie eine neue Version übernimmt:
module "network" {
source = "git::ssh://git@github.com/acme/tf-modules.git//network?ref=v2.3.0"
# ...
}
Ein Modul-Update rollt so kontrolliert von Dev nach Prod — statt alle Umgebungen gleichzeitig zu treffen.
Was Module nicht tun sollten
- Kein Provider-Block im Modul. Provider werden vom aufrufenden Root-Modul gesetzt und übergeben, sonst lässt sich das Modul nicht mehr für mehrere Regionen oder Accounts verwenden.
- Kein Backend im Modul. State-Konfiguration gehört zur Umgebung.
- Keine harten Namen. Ressourcennamen aus Variablen bilden, sonst kollidiert das zweite Deployment mit dem ersten.
Fazit
Guter Modul-Schnitt zeigt sich nicht am ersten Tag, sondern beim vierten Kunden oder der dritten Region. Wer fachlich schneidet, Schnittstellen dokumentiert und Module versioniert, baut Infrastruktur, die sich erweitern lässt statt neu geschrieben zu werden.
Sie möchten Ihren Terraform-Code aufräumen oder von Grund auf modular aufbauen? Wir unterstützen Sie dabei.