"Zur Hölle mit der Sichtbarkeit - Hauptsache es läuft!"
Das ist die Einstellung, mit der sich die meisten Platform-Engineering-Teams ins Verderben stürzen. Wie beim Kochen mit verbundenen Augen in einer fremden Küche: Es geht eine Zeit lang gut, aber wenn's anbrennt, dann richtig.
Und nichts ist schlimmer als der Blick in ratlose Gesichter, wenn die Hütte brennt.
In den beiden vorhergegangenen Teilen waren Abhängigkeits-Schmerzen unser Hauptthema. Schauen wir uns heute an, welche Möglichkeiten wir haben, ein in den Brunnen gefallenes Kind noch lebend herauszufischen.
Möglichkeiten zur Darstellung von Modulabhängigkeiten
Das erste Problem bei komplexen Modulhierarchien ist die Sichtbarkeit. Auf dem Papier gibt es dafür terraform graph:
terraform graph | dot -Tpng > graph.png
Der Output von Terraforms terraform graph ist zwar gut gemeint, sieht nach Umwandlung in eine Grafik aber ziemlich unbrauchbar aus. Terraform unterstützt kein GraphML, kein GrapAR, kein TGF, kein GML, kein JSON, sondern nur das .dot-Format, welches zwar in der Codegenerierung und Automation unter Linux verbreitet ist, aber in Sachen Visualisierung ein von keiner Enterprise-Applikation für Modellierung oder Datenanalyse unterstützt wird.
Zur Umwandlung von .dot in eine Grafik benötigen Sie das graphviz-Paket, und das sieht dann zum Beispiel so aus:
Ich denke, das hat weniger Ähnlichkeit mit meiner Netzwerkinfrastruktur als mit einem Massencrash beim Formel-1-Rennen in Monaco, wenn direkt nach dem Start alle gleichzeitig versuchen, in die erste Kurve abzubiegen. Benutzerfreundlichkeit geht anders.
Und selbst wenn Sie diese .dot-Datei mit viel Manpower und Feintuning in ein klares Schaubild verwandelt bekommen haben, können Sie damit immer noch genausoviel sinnvoll anfangen wie mit der originalen erzeugten .dot-Datei in Enterprise-Umgebungen, nämlich nichts.
Bleibt noch terraform show, denn dieses Kommando listet alle Ressourcen auf. Bringt Ihnen aber nicht viel, denn Abhängigkeiten zu visualisieren schafft es auch nicht.
Hier kommt deshalb ein kleines Skript, welches sein Bestes versucht, um den Output von terraform graph irgendwie in der Shell darzustellen. Mit diesem Skript sehen Sie sofort, dass Modul X vom Remote State Y abhängt.
Es heißt terraform-graph-visualizer.sh und anstelle Ihnen jetzt hier über 200 Zeilen Bash-Code zu präsentieren, gebe ich Ihnen die URL des Github-Repositories: GitHub - ICT-technology/terraform-graph-visualizer
Der Output aus dem obigen Graphen sieht dann so aus (massiv verkürzte Darstellung):
$ terraform graph | ~/bin/terraform-graph-visualizer.sh
╔══════════════════════════════════════════════════════════╗
║ TERRAFORM GRAPH VISUALIZATION ║
╚══════════════════════════════════════════════════════════╝ Analyzing: stdin (terraform graph) GRAPH STATISTICS
═════════════════
├─ Total Nodes: 96
├─ Total Edges: 63
├─ Modules: 10
└─ Data Sources: 3 TERRAFORM MODULES
══════════════════
├─ module.drg
│ ├─ oci_core_drg_attachment.this
│ └─ oci_core_drg.this ├─ module.drg_attachments
│ ├─ oci_core_drg_attachment.this
│ └─ oci_core_drg.this
[...] DATA SOURCES
═════════════
├─ data.terraform_remote_state.icttechnology_buckets
├─ data.terraform_remote_state.icttechnology_compartments
├─ data.terraform_remote_state.tfstate-icttechnology_root 🔗 DEPENDENCY RELATIONSHIPS
═══════════════════════════
┌─ data.terraform_remote_state.icttechnology_compartments
│ depends on:
│ ├─ module.drg.oci_core_drg.this
│ ├─ module.drg.oci_core_drg_attachment.this
│ ├─ module.drg_attachments.oci_core_drg.this
│ ├─ module.natgw.data.oci_core_services.services
│ ├─ module.natgw.oci_core_vcn.this
│ ├─ module.servicegw.data.oci_core_services.services
│ ├─ module.servicegw.oci_core_nat_gateway.this
│ ├─ module.servicegw.oci_core_vcn.this
│ ├─ module.vcn.data.oci_core_services.services
│ ├─ module.vcn.oci_core_nat_gateway.this
│ └─ module.vcn.oci_core_vcn.this
│
┌─ module.drg.oci_core_drg_attachment.this
│ depends on:
│ ├─ module.drg_attachments.oci_core_drg_attachment.this
│ ├─ module.route_table_bastion-classic.oci_core_route_table.this
│ └─ module.route_table_vcn_FRA.oci_core_route_table.this
[...] Graph visualization complete!
Immer noch nicht schön, aber immerhin lesbar. Und sie sehen hier außer reinen Abhängigkeiten auch sofort:
- Remote-State-Bezüge
- Fan-in/Fan-out auffälliger Module
- Data-Source-Hotspots
Policy-as-Code: Wenn Sentinel den gröbsten Unsinn verhindert
Mit Terraform Enterprise steht Sentinel als Policy-as-Code-Engine zur Verfügung. Sentinel stellt sicher, dass Unternehmensrichtlinien und regulatorische Vorgaben eingehalten werden - das heißt, es verhindert unsere wirklich dummen Entscheidungen, bevor sie Schaden anrichten können.
Sentinel Policy für Modul-Versionspinning
Mit einer Policy wie dieser können Sie erzwingen, dass Module auf Versionen gepinnt werden:
import "tfconfig/v2" as tfconfig import "strings" // Heuristic: source is VCS if it has a git:: prefix OR a URI scheme present is_vcs = func(src) { strings.has_prefix(src, "git::") or strings.contains(src, "://") } // extract ref from the query (if present) ref_of = func(src) { // very simple extraction: everything after "?ref=" until the end // returns "" if not present idx = strings.index_of(src, "?ref=") idx >= 0 ? strings.substring(src, idx+5, -1) : "" } // Policy: all non-local modules must be versioned mandatory_versioning = rule { all tfconfig.module_calls as name, module { // local modules (./ or ../) are excluded if strings.has_prefix(module.source, "./") or strings.has_prefix(module.source, "../") { true } else if is_vcs(module.source) { // VCS modules: ref must be vX.Y.Z ref = ref_of(module.source) ref matches "^v[0-9]+\\.[0-9]+\\.[0-9]+$" } else { // Registry modules: version argument must be X.Y.Z module.version is not null and module.version matches "^[0-9]+\\.[0-9]+\\.[0-9]+$" } } } // Detailed validation for OCI modules validate_oci_modules = rule { all tfconfig.module_calls as name, module { if strings.contains(module.source, "oci-") { if is_vcs(module.source) { // strict path structure + SemVer tag module.source matches "^git::.+//base/oci-.+\\?ref=v[0-9]+\\.[0-9]+\\.[0-9]+$" } else { module.version is not null and module.version matches "^[0-9]+\\.[0-9]+\\.[0-9]+$" } } else { true } } } main = rule { mandatory_versioning and validate_oci_modules }
Der erste Schritt ist damit gemacht.
Noch ein Schritt mehr: Allow- und Deny-Listen mittels Policy-as-Code
Falls Sie noch einen draufsetzen wollen, bringt eine Policy wie die jetzt folgende noch zusätzlichen Mehrwert. Damit können Sie eine Allowlist für Modulquellen plus eine Denylist für fehlerhafte oder verwundbare Versionen einführen und das Ganze optional mit „Advisory“-Hinweisen flankieren.Sie sollten diese Policy jetzt aber nicht 1:1 übernehmen, sondern an Ihre eigenen Use Cases und vor allem real existierende Versionen und Advisories anpassen, denn sonst greifen die Regeln nicht:
import "tfconfig/v2" as tfconfig import "strings" // Helper functions is_local = func(src) { strings.has_prefix(src, "./") or strings.has_prefix(src, "../") } is_vcs = func(src) { strings.has_prefix(src, "git::") or strings.contains(src, "://") } ref_of = func(src) { i = strings.index_of(src, "?ref=") i >= 0 ? strings.substring(src, i+5, -1) : "" } // Allowlist of module sources allowed_module_sources = [ "git::https://gitlab.ict.technology/modules//", "app.terraform.io/ict-technology/" ] // Banned lists separated for VCS tags and Registry versions banned_vcs_tags = [ "v1.2.8", // CVE-2024-12345 "v1.5.2" // Critical bug in networking ] banned_registry_versions = [ "1.2.8", "1.5.2" ] // Rule: only allowed sources OR local modules only_allowed_sources = rule { all tfconfig.module_calls as _, m { is_local(m.source) or any allowed_module_sources as pfx { strings.has_prefix(m.source, pfx) } } } // Rule: no banned versions (VCS: tag in ref, Registry: version argument) no_banned_versions = rule { all tfconfig.module_calls as _, m { if is_local(m.source) { true } else if is_vcs(m.source) { t = ref_of(m.source) not (t in banned_vcs_tags) } else { // Registry m.version is string and not (m.version in banned_registry_versions) } } } // Advisory: warning for old major versions (set policy enforcement level to "advisory") warn_old_modules = rule { all tfconfig.module_calls as _, m { if is_local(m.source) { true } else if is_vcs(m.source) { r = ref_of(m.source) // If v1.*, then print warning, rule still passes strings.has_prefix(r, "v1.") ? (print("WARNING: Module", m.source, "uses v1.x - consider upgrading to v2.x")) or true : true } else { // Registry m.version is string and strings.has_prefix(m.version, "1.") ? (print("WARNING: Module", m.source, "uses 1.x - consider upgrading to 2.x")) or true : true } } } // Main rule: hard checks must pass main = rule { only_allowed_sources and no_banned_versions }
Testing Framework: Automatisierte Guardrails in Terraform 1.10+
Sentinel-Policies sind das eine – aber manchmal wollen Sie projektnahe Regeln direkt dort prüfen, wo der Code lebt. Seit Terraform 1.10 steht dafür das native Testing Framework bereit. Damit lassen sich kleine, fokussierte Tests schreiben, die Ihre Module gegen Fehlkonfigurationen abhärten. Keine externe Engine, kein Overhead – einfach deklarativ im Projekt.
# tests/guardrails.tftest.hcl variables { max_creates = 50 max_changes = 25 } run "no_destroys_in_plan" { command = plan assert { condition = length([ for rc in run.plan.resource_changes : rc if contains(rc.change.actions, "delete") ]) == 0 error_message = "Plan contains deletions. Please split the change or use an approval workflow." } } run "cap_creates" { command = plan # Counts pure creates, not replacements assert { condition = length([ for rc in run.plan.resource_changes : rc if contains(rc.change.actions, "create") && !contains(rc.change.actions, "delete") ]) <= var.max_creates error_message = format( "Too many new resources (%d > %d) in one run – split into smaller batches.", length([for rc in run.plan.resource_changes : rc if contains(rc.change.actions, "create") && !contains(rc.change.actions, "delete")]), var.max_creates ) } } run "cap_changes" { command = plan assert { condition = length([ for rc in run.plan.resource_changes : rc if contains(rc.change.actions, "update") ]) <= var.max_changes error_message = "Too many updates on existing resources - blast radius is too high." } } run "cap_replacements" { command = plan assert { condition = length([ for rc in run.plan.resource_changes : rc if contains(rc.change.actions, "create") && contains(rc.change.actions, "delete") ]) == 0 error_message = "Plan contains replacements (create+delete) - please review and minimize them." } }
Damit schlagen Sie zwei Fliegen mit einer Klappe: Ihre Engineers können auf Projektebene kontrollieren, dass Module sauber versioniert sind, und Ihre CI/CD-Pipeline bekommt einen zusätzlichen Sicherheitsgurt. Knallt es hier, dann wenigstens früh und bevor es in der Produktion aufschlägt.
Aber eine Erklärung dürfte vonnöten sein.
Warum das funktioniert, in ganz einfach:
- Terraform führt für jeden run ein plan aus. Das Testing Framework stellt das Ergebnis als strukturierte Daten unter run.plan bereit. Das ist kein reiner Textdump, sondern ein Objekt, in dem unter anderem resource_changes steckt.
- run.plan.resource_changes ist eine Liste, in der jeder Eintrag einem geplanten Ressourcenvorgang entspricht. Zu jedem Eintrag gibt es change.actions. Das ist eine Liste von Aktionen, die Terraform für diese Ressource plant. Mögliche Inhalte sind zum Beispiel:
- nur "create" wenn eine Ressource neu erstellt wird,
- nur "update" wenn eine bestehende Ressource geändert wird,
- "create" und "delete" zusammen, wenn Terraform ersetzt, also erst löscht und dann neu erstellt. Das ist das klassische Replacement.
- Die Assertions sind einfache Zählregeln über diese Liste:
- no_destroys_in_plan filtert alle Einträge heraus, deren actions die Zeichenkette "delete" enthalten. Wenn die Menge nicht leer ist, schlägt der Test fehl. Damit verhindern Sie harte Löschungen im Plan.
- cap_creates zählt nur echte Neuanlagen, also Einträge mit "create" ohne gleichzeitiges "delete". Replacements werden damit bewusst nicht als reine Creates gezählt. Die Fehlermeldung nutzt format(...), damit die eingebetteten Anführungszeichen kein Syntaxproblem verursachen und Sie gleichzeitig die Ist-Zahl vs. Grenzwert sehen.
- cap_changes limitiert die Anzahl von Updates, also Änderungen an bestehenden Ressourcen.
- cap_replacements fängt explizit die riskanten Replacements ab, also die Kombination aus "create" und "delete" bei derselben Ressource. Diese Fälle sind oft die größten Risikotreiber, weil sie kurzzeitig Downtime oder Seiteneffekte erzeugen können.
- Die variables { ... } am Anfang setzen Grenzwerte, die in allen Runs verfügbar sind. So können Sie die Policy je Pipeline oder Umgebung anpassen, ohne die Testlogik zu ändern.
- Der Test braucht keine projektinternen Spezialkonstrukte. Er arbeitet nur mit dem standardisierten Planmodell, das Terraform bereitstellt. Deshalb ist er portabel und (hoffentlich) sofort nutzbar - sofern ich nicht wieder irgendwelche Fehler aus Versehen eingebaut habe.
Falls Sie zusätzlich erwartete Precondition-Fehlschläge testen möchten, können Sie das tun, benötigen dafür aber benannte check-Blöcke in Ihrem Code. Beispiel:
# In modules or root check "api_limits_reasonable" { assert { condition = var.instance_count <= 200 error_message = "Instance-Batch zu groß." } } # and a test which deliberately violates the precondition run "expect_api_limit_breach" { command = plan variables { instance_count = 1000 } expect_failures = [ check.api_limits_reasonable ] }
Ohne solche benannten Checks würde ein entsprechender Test ins Leere zeigen. Für Modul-Versionierung oder Version-Constraints ist das Testing Framework nicht geeignet, weil das Framework auf Planinhalte schaut, nicht auf Modul-Metadaten. Dafür eignen sich Sentinel-Policies oder ein CI-Skript, das die Modulquellen und ?ref= Tags bzw. version-Constraints analysiert.
Wenn das Kind schon im Brunnen liegt
Angenommen, jetzt ist ihr State bereits kaputt und es gibt inkorrekte Abhängigkeiten. Dann wird es vielleicht Zeit für außergewöhnliche Maßnahmen.Hier eine Notfallmaßnahme für die ganz Harten, wieder nur als Link zum Git-Repository: Github - ICTtechnology/module-state-recovery
Das Skript nimmt Ihnen die mühselige Kleinarbeit ab und spürt von selbst die Module auf, die im Terraform-State Ärger machen. Es legt brav ein Backup an, fährt einen Planlauf, gibt diesen als JSON aus und schaut dort ganz genau hin, welche Module für Ärger sorgen.Die betroffenen Ressourcen werden in einer Vorschau angezeigt. Wenn Sie möchten, können Sie sogar eine Mapping-Datei nutzen, um State-Adressen automatisch sauber zu verschieben, bevor der Rest aufgeräumt wird.Im Normalbetrieb passiert nichts Schlimmes – alles läuft im Dry-Run. Erst wenn Sie bewusst mit CONFIRM=1 und I_UNDERSTAND=YES loslegen, greift das Skript wirklich ein - dann aber richtig und mit der großen Axt.Damit ist es ein hilfreiches Werkzeug für Training und Tests, um Modulkonflikte zu durchschauen und in den Griff zu bekommen. Für den produktiven Betrieb ist es aber ausdrücklich nicht gedacht.
Checkliste für robuste Modulabhängigkeiten
✅ Versionsstrategie
[ ] Semantic Versioning für alle Basis- und Service-Module. Wer hier kreativ wird, zahlt später doppelt.
[ ] Pinning-Strategie pro Modul-Ebene (Root, Service, Basis). Wer "latest" schreibt, hat verloren.
[ ] Compatibility Matrix für Modulversionen ist vorhanden und gepflegt.
[ ] Dependency Lock-Mechanismus oder CI-Skripte zur Versionsprüfung sind integriert.
✅ Governance
[ ] Modulversionsverfolgung ist in CI/CD integriert.
[ ] Sentinel-Policies für erlaubte Modul-Quellen und Versionsranges sind etabliert.
[ ] Allow- und Deny-Listen für bekannte fehlerhafte Modulversionen sind eingerichtet.
[ ] Breaking-Change-Benachrichtigungen (Release Notes, Advisory-Mechanismen) sind etabliert.
[ ] Modul-Inventar wird regelmäßig aktualisiert und geprüft.
✅ Monitoring
[ ] Audit Logging für Modulupdates und State-Veränderungen ist aktiviert.
[ ] Frühwarnsysteme für destruktive Änderungen (Plan-Analyse, Exit Codes) sind implementiert. Wenn diese nicht existieren, sehen Sie den Knall erst im Betrieb.
[ ] Alerting bei unerwarteten Moduländerungen oder Dependency Drift ist vorhanden und aktiv.
✅ Recovery
[ ] Verfahren für State Surgery und Adress-Migration ist definiert, dokumentiert und kommuniziert.
[ ] Rollback-Strategie für fehlerhafte Modulupdates ist vorhanden.
[ ] Emergency Response Playbooks für kritische Modulprobleme sind erstellt und kommuniziert.
[ ] Team-Training für Troubleshooting von Modulabhängigkeiten ist etabliert.Ja, das wollen Sie ab einer gewissen Größe ihrer automatisierten Infrastruktur alles haben. Notfalls auch gegen den Widerstand einiger Engineers, die sich selbst als Seniors bezeichnen.Denn nichts ist schlimmer als ratlose Gesichter, wenn die Hütte brennt.