Terraform Remote State für Platform-Teams
State Locking, Aufteilung und Blast Radius.
von PlatPrep
Zwei Entwickler, ein Terraform-State. Beide führen gleichzeitig terraform apply aus. Der State wird korrupt. Das klingt nach einem Anfängerproblem, kommt aber in der Praxis vor — besonders wenn Remote State zu spät eingerichtet wird oder eine neue Person ins Team kommt und es nicht weiß.
Remote State ist nicht optional, sobald mehr als eine Person an einer Terraform-Konfiguration arbeitet. S3 für AWS, GCS für GCP, Terraform Cloud oder ein S3-kompatibler Store on-prem — egal welches Backend, Hauptsache der State liegt nicht lokal und ist nicht in Git. Letzteres ist doppelt wichtig, weil der State sämtliche Attribute der verwalteten Ressourcen im Klartext enthält — inklusive Datenbankpasswörtern und allem, was Provider als Output zurückgeben. Ein State in Git ist ein Secrets-Leak mit Versionshistorie. Verschlüsselung at rest und enge Zugriffsrechte auf das State-Bucket sind deshalb Pflicht, nicht Kür.
Locking: der Teil, der Korruption tatsächlich verhindert
Locking bedeutet: vor jedem apply wird ein Lock gesetzt, konkurrierende Läufe sehen ihn und brechen ab. Beim S3-Backend lief das historisch über eine separate DynamoDB-Tabelle; seit Terraform 1.10 unterstützt das S3-Backend natives Locking über Conditional Writes (use_lockfile), womit die DynamoDB-Tabelle perspektivisch entfällt. Welche Variante du nutzt, hängt von deiner Terraform-Version ab — wichtig ist nur, dass eine aktiv ist.
Den Lock wirst du auch von der unangenehmen Seite kennenlernen: ein abgebrochener CI-Run lässt einen stale Lock zurück, der nächste apply hängt. terraform force-unlock <LOCK_ID> löst das — aber nur ausführen, wenn sicher ist, dass wirklich kein Lauf mehr aktiv ist. Wer den Lock eines laufenden apply bricht, hat genau die Korruption produziert, die das Locking verhindern sollte.
State-Aufteilung: Blast Radius vor Bequemlichkeit
Alles in einem State ist ein Anti-Pattern, das mit der Organisation mitwächst: ein plan über tausende Ressourcen dauert mehrere Minuten, weil Terraform jede Ressource gegen die Cloud-API refresht. Ein Fehler blockiert alle. Und ein fehkonfigurierter destroy hat den maximalen Schaden.
Besser ist die Aufteilung nach Domäne und Änderungsfrequenz: network/, eks-cluster/, team-namespaces/ — jeweils mit eigenem State-Key, getrennt pro Environment. Das Netzwerk ändert sich selten und hat hohen Blast Radius; Team-Namespaces ändern sich täglich und sind harmlos. Diese beiden Dinge gehören nicht in denselben State, weil sonst jede harmlose Änderung das Risiko der gefährlichen trägt.
Workspaces sind dafür übrigens das falsche Werkzeug: sie trennen State-Instanzen derselben Konfiguration (etwa dev/staging/prod), nicht verschiedene Infrastruktur-Domänen.
Referenzen zwischen den Bereichen löst die terraform_remote_state Data Source — das Netzwerk-Modul published die VPC-ID als Output, das EKS-Modul liest sie:
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "acme-tf-state"
key = "prod/network/terraform.tfstate"
region = "eu-central-1"
}
}
Das schafft explizite Abhängigkeiten — aber auch Coupling: wer einen Output umbenennt, bricht stillschweigend alle Konsumenten. Zwei Auswege: Outputs als stabilen Vertrag behandeln (umbenennen ist ein Breaking Change mit Ankündigung), oder die Kopplung über Data Sources auf die echten Ressourcen auflösen (aws_vpc per Tag suchen statt State lesen). Terragrunt modelliert dieselbe Beziehung mit dependency-Blocks etwas expliziter, kauft das aber mit einem zusätzlichen Tool ein.
Und wenn der Monolith-State schon existiert? Dann steht eine State-Chirurgie an: Ressourcen mit terraform state mv aus dem alten State exportieren und im neuen per import-Block (seit Terraform 1.5 deklarativ im Code statt imperativ per CLI) wieder übernehmen — Ressource für Ressource, mit plan-Kontrolle nach jedem Schritt, dass nichts zerstört oder neu erstellt würde. Das ist mühsame, riskante Arbeit. Genau deshalb lohnt es sich, die Aufteilung früh zu entscheiden statt sie später nachzuholen.
CI, IAM und Module: die organisatorische Hälfte
Sobald Terraform in CI läuft, gilt: Menschen schreiben Code und lesen Plans, nur die Pipeline applied. Der Standard-Flow — plan im Pull Request als Kommentar, apply nach Merge — macht jede Infrastrukturänderung reviewbar und auditierbar.
Die CI-Identität braucht Least-Privilege-IAM, und ja, das ist mühsam: du musst verstehen, was die Konfiguration wirklich tut, und die Policy mitpflegen. Genau das ist der Wert. Wer der Pipeline AdministratorAccess gibt, hat einen Review-Layer aufgegeben — ein kompromittiertes CI-Token ist dann ein kompromittiertes Cloud-Konto. Mit OIDC-Federation statt statischen Keys (etwa GitHub Actions OIDC zu AWS) entfällt wenigstens das langlebige Credential.
Interne Module gehören versioniert und gepinnt — per Registry oder schlicht Git-Tags (ref=v1.4.0 in der Source-URL). Ein ungepinntes Modul bedeutet: irgendein terraform init zieht irgendwann Breaking Changes, und zwar im ungünstigsten Moment. Dasselbe gilt für Provider-Versionen in required_providers.
Die Grundlagen von Backends, Modulen und Workspaces vertieft der Terraform-Lernpfad — und wer Terraform mit Kubernetes-nativer Provisionierung vergleichen will, findet im Artikel zu Crossplane Compositions die Gegenposition.