Terraform AzureRM 5.0: Die fünf Bruchstellen, die Bestands-Landing-Zones treffen
Warum ein Provider-Major kein Dependency-Bump ist, welche fünf Bereiche einer typischen Azure Landing Zone brechen und wie ein Upgrade-Vorgehen aussieht, das Produktion nicht gefährdet.

Ein Major-Release des AzureRM-Providers wird in den meisten Teams wie ein Dependency-Bump behandelt. Dependabot öffnet dann einen Pull Request, die Pipeline ist grün und jemand merged.
Das Problem daran ist, dass auch in den neuen Provider Versionen Bugs, Fehler und Breakting Changes enthalten sein können. Der grüne Build sagt nur, dass der Code parst. Er sagt nichts darüber, ob der neue Provider die bestehenden State-Einträge noch so interpretiert wie die alte Version. Wenn ein Attribut umbenannt wurde und Terraform das alte Feld nicht mehr findet, plant es keinen Update, sondern ein Replace. Bei einem Storage Account mit Daten, einem Key Vault mit Zertifikaten oder einem AKS-Cluster mit produktiven Workloads ist das dann keine kleine Anpassungen, sondern ggf. eine Katastrophe im Team.
Provider-Updates gehören geplant wie eine Migration oder die Einführung eines neuen Services.
Was ein Major-Release beim Provider technisch bedeutet
Ich schreibe hier bewusst keine Changelog-Analyse. Die vollständige Liste der Änderungen pflegt HashiCorp selbst und die ist beim Upgrade eure Primärquelle. Was in der Praxis fehlt, ist die Einordnung. Welche Klasse von Änderung erzeugt welches Risiko im State und welche Ressourcen einer typischen Landing Zone sind davon betroffen.
Ich würde dabei in die folgenden drei Änderungsklassen unterscheiden.
Schema-Änderungen führen zu Replace statt Update
Terraform vergleicht beim Plan die Konfiguration, den State und die Realität in Azure. Der Provider definiert über sein Schema, welche Attribute existieren, welchen Typ sie haben und ob eine Änderung in-place möglich ist oder ForceNew auslöst.
Wenn ein Major-Release ein Attribut umbenennt, entfernt oder von einem block in eine list umbaut, passiert Folgendes: Der State enthält weiterhin die alte Struktur. Der neue Provider liest sie, findet kein passendes Schema-Feld und behandelt den Wert als nicht gesetzt. Steht das Attribut in der Konfiguration unter dem neuen Namen, sieht Terraform eine Änderung von "nicht gesetzt" auf "gesetzt". Ist dieses Attribut ForceNew, ist das Ergebnis ein Destroy-and-Create.
Genau dieses Muster hat es bei jedem bisherigen Major gegeben. In 3.0 wurde der addon_profile-Block bei azurerm_kubernetes_cluster aufgelöst und auf Top-Level-Attribute wie oms_agent und azure_policy_enabled verteilt. In 4.x wurde bei den Storage-Ressourcen der Bezug von storage_account_name auf storage_account_id umgestellt. Beides sind auf den ersten Blick kleine Änderungen, die aber katastrophal enden können.
Deprecation-Hinweise zeitnah angehen
Deprecation-Warnungen erscheinen im Minor-Release und verschwinden im Major. Teams, die Warnungen in der Pipeline ignorieren, sammeln über Monate genau die Änderungen an, die beim Major-Sprung dann gleichzeitig fällig werden.
Das ist der Grund, warum ich in Reviews immer zuerst in die Pipeline-Logs schaue und nicht in den Code. Die Logs der letzten Wochen geben schnell Aufschluss darüber was beim nächsten Major brechen könnte.
Default-Werte könnten Probleme machen
Die gefährlichste Klasse sind geänderte Defaults, weil sie im Code unsichtbar bleiben. Wenn ein Attribut, das ihr nie explizit gesetzt habt, seinen Standardwert ändert, produziert der Plan eine Änderung an einer Ressource, die niemand angefasst hat. Ein bekanntes Beispiel aus 4.x ist default_outbound_access_enabled bei Subnetzen. Das ist ein Feld, das vorher nicht existierte und dessen Verhalten Netzwerk-Egress betrifft.
In der Praxis bedeutet das bei einem Provider-Upgrade ein Plan der unerwartet 40 Änderungen erzeugt, obwohl im Repository seit drei Wochen kein Commit lag. Wenn dann bei 38 Änderungen davon die Verschiebung der Default-Werte ist, fällt das in einem Merge Request nicht auf.
Die fünf Bereiche mit dem höchsten Risiko
Eine typische Landing Zone besteht aus wenigen wiederkehrenden Bausteinen. Diese fünf sind bei jedem Major-Release die Kandidaten, die ich zuerst prüfe.
| Bereich | Typisch betroffene Konstrukte | Risiko bei Replace |
|---|---|---|
| Netzwerk | azurerm_virtual_network mit inline subnet, NSG- und Route-Table-Assoziationen, Subnetz-Delegationen | Hoch. Ein VNet-Replace hängt an Peerings, Private Endpoints und DNS |
| Storage | Netzwerk-Regeln, identity-Block, Container- und Share-Referenzen | Sehr hoch. Datenverlust bei Destroy |
| Key Vault | Access Policies gegen RBAC, purge_protection, soft_delete | Sehr hoch. Purge Protection verhindert die Neuanlage unter gleichem Namen |
| AKS | identity- und network_profile-Struktur, Node-Pool-Attribute | Hoch. Cluster-Replace bedeutet Ausfall aller Workloads |
| Provider-Block | Authentifizierung, subscription_id, features, Provider-Registrierung | Mittel. Bricht früh und laut, dafür überall gleichzeitig |
Netzwerk: Inline-Subnetze können Probleme machen
Wer Subnetze als inline subnet-Blöcke im azurerm_virtual_network definiert, hat bei jedem Schema-Wechsel ein Problem, weil die gesamte Subnetz-Liste als ein Attribut des VNets verwaltet wird. Eine Änderung an einem einzelnen Eintrag berührt die Struktur der gesamten Liste.
Die saubere Variante ist die separate Ressource azurerm_subnet mit expliziten Assoziationsressourcen für NSG und Route Table. Das ist mehr Code, isoliert aber den Blast Radius pro Subnetz. In meinem Terraform-Framework habe ich Subnetze deshalb konsequent als eigene Ressourcen in der Core-Schicht modelliert und weise sie über eine Map zu, statt sie im VNet-Modul zu verschachteln. Beim letzten Major-Sprung war das der Unterschied zwischen einem betroffenen Modul und einer betroffenen Landing Zone.
Besondere Aufmerksamkeit brauchen Delegationen. Ein Subnetz mit einer Delegation an Microsoft.DBforPostgreSQL/flexibleServers oder Microsoft.App/environments lässt sich nicht ohne Weiteres neu anlegen, solange der delegierte Dienst darin läuft.
Storage: Netzwerkregeln und Identity
Storage Accounts sind aus zwei Gründen kritisch. Erstens liegt oft der Terraform-State in Storage Accounts selbst. Zweitens ist ein Destroy-and-Create bei einem Storage Account nicht reversibel.
Die typischen Bruchpunkte sind der Übergang von der eingebetteten network_rules zur separaten Ressource, der Umgang mit allow_nested_items_to_be_public und die Frage, ob Container über Name oder über ID referenziert werden. Prüft zusätzlich, ob euer State-Backend-Storage überhaupt im selben Terraform verwaltet wird. Wenn ja, ist das ein eigener Migrationsschritt mit hoher Priorität.
Key Vault: Access Policies gegen RBAC
Der Provider bildet zwei Berechtigungsmodelle ab die klassischen Access Policies und das RBAC-Modell über enable_rbac_authorization. Wer beides mischt, könnte die Übersicht verlieren.
Kritisch wird es, weil Key Vaults mit aktivierter Purge Protection nach einem Destroy in Soft-Delete landen und der Name für die Aufbewahrungsdauer blockiert bleibt. Ein versehentliches Replace ist damit nicht in fünf Minuten korrigiert, sondern erfordert einen re-import des soft-deleted Key Vaults oder die Erstellung eines neuen Key Vaults unter einem neuen Namen, der wiederum in jeder abhängigen Konfiguration nachgezogen werden muss.
AKS: Identity- und Netzwerk-Profile
AKS ist die Ressource mit der höchsten Schema-Volatilität im gesamten Provider, weil Azure den Dienst schneller weiterentwickelt als jede andere Plattformkomponente. Der identity-Block, network_profile, die Node-Pool-Attribute und die Addon-Konfiguration wurden über die Majors hinweg mehrfach umgebaut.
Meine Regel dabei ist AKS bekommt einen eigenen Migrationsschritt, niemals gemeinsam mit Netzwerk oder Storage. Der Plan wird dann vor dem Apply Zeile für Zeile gelesen.
Provider-Block: bricht früh, aber dafür überall
Die Provider-Konfiguration selbst ändert sich bei Majors regelmäßig. In 4.0 wurde subscription_id zur Pflichtangabe und die Provider-Registrierung neu geregelt. Das ist die angenehmste Kategorie, weil sie sofort und laut fehlschlägt. Sie trifft nur eben alle Stages gleichzeitig, inklusive der Pipeline-Authentifizierung über Workload Identity Federation.
Vorgehen: Pinnen, Non-Prod, Migrationsplan pro Modul
Schritt 1: Provider-Constraint explizit setzen
Bevor irgendetwas anderes passiert, muss die aktuell laufende Version im Code stehen. Nicht als Pessimistic Constraint über eine ganze Major-Reihe, sondern als klare Untergrenze mit ausgeschlossenem Major-Sprung.
terraform {
required_version = "~> 1.9"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.30"
}
}
}Erwartetes Ergebnis: terraform init zieht nur noch Versionen innerhalb der 4.x-Reihe. Ein terraform init -upgrade in einer Pipeline kann keinen Major-Sprung mehr auslösen. Zusätzlich gehört die .terraform.lock.hcl ins Repository, damit auch die Provider-Hashes reproduzierbar sind.
Schritt 2: Upgrade isoliert in einer Non-Prod-Stage
Der Sprung wird ausschließlich in einer Stage gefahren, die produktionsähnlich aufgebaut ist. Produktionsähnlich heißt, es werden die gleichen Module, gleichen Modulversionen und gleiche Ressourcentypen verwendet. Eine Dev-Umgebung, in der Private Endpoints deaktiviert und AKS durch eine Container App ersetzt wurde, ist als Testumgebung in dem Kontext wertlos.
Wenn es diese Stage nicht gibt, ist das das eigentliche Problem. Dann ist das Provider-Upgrade nicht euer dringenstes Thema.
Schritt 3: Plan diffen und Replace-Operationen zählen
Der Plan wird nicht gelesen, sondern maschinell ausgewertet. Menschen überlesen ein -/+ in 300 Zeilen Ausgabe.
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
# Alle Ressourcen, die zerstört oder ersetzt werden
jq -r '.resource_changes[]
| select(.change.actions | index("delete"))
| "\(.change.actions | join(",")) \(.address)"' tfplan.jsonDas erwartete Ergebnis ist dann eine Liste, die im Idealfall leer ist. Ist sie es nicht, ist jede Zeile einzeln zu prüfen. Für jede betroffene Adresse gibt es genau drei mögliche Antworten. Der Replace ist fachlich in Ordnung. Der Replace lässt sich durch moved-Blöcke oder Import vermeiden oder das Modul wird vorerst nicht migriert.
moved {
from = azurerm_subnet.app
to = azurerm_subnet.app["workload"]
}Erwartetes Ergebnis: Terraform schreibt den State um, ohne Azure anzufassen. Der Plan zeigt danach 0 to add, 0 to change, 0 to destroy.
Schritt 4: Modul für Modul migrieren
Ein Big-Bang-Upgrade über die gesamte Landing Zone passt, wenn ihr genau eine Stage betreibt, keine produktiven Daten haltet und der Plan nach dem Upgrade unverändert leer ist. Das ist der Fall bei frisch aufgebauten Umgebungen.
Nicht passend ist der Big-Bang, sobald mehrere Stages, geteilte Netzwerk-Ressourcen oder datentragende Dienste im Spiel sind. Dann gilt die Reihenfolge von unten nach oben.
| Reihenfolge | Ebene | Begründung |
|---|---|---|
| 1 | State-Backend und Provider-Konfiguration | Bricht laut und blockiert alles Weitere |
| 2 | Governance, Policies, Management Groups | Kaum Datenbezug, gute Testfläche für das Upgrade selbst |
| 3 | Netzwerk (Hub) | Basis für alles darüber, aber hohe Abhängigkeit. Nur nach sauberem Plan |
| 4 | Shared Services: Key Vault, Log Analytics, Container Registry | Datentragend, Purge Protection beachten |
| 5 | Workload-Landing-Zones: AKS, App Services, Datenbanken | Höchste Schema-Volatilität, eigener Change pro Stack |
In der Praxis kannst du in dieser Reihenfolge Modul für Modul die Änderungen analysieren und relativ schnell die betroffenen Umgebungen abarbeiten. Kein einziger ungeplanter Replace. Der Aufwand liegt dann in der Analyse, statt einem aufwändigen Rollback bei Problemen.
Warum Version-Pinning alleine nicht reicht
Version-Pinning ist notwendig. Pinning als Dauerzustand verschiebt den Aufwand aber auf unbestimmte Zeit.
Der Provider ist der Übersetzer zwischen eurem Code und der Azure Resource Manager API. Neue Azure-Features erscheinen im Provider nur in aktuellen Versionen. Wer auf einer alten Major-Reihe sitzt, kann irgendwann ein Feature nicht mehr über Terraform abbilden, das fachlich gebraucht wird. Der übliche Ausweg ist dann ein Klick im Portal oder ein azapi-Workaround. Beides erzeugt Drift, den der nächste Plan wieder einsammelt.
Der zweite Effekt dass bei zwei Majors Rückstand das nicht doppelten Aufwand bedeutet, sondern deutlich mehr, weil die Änderungen sich überlagern und die Zwischenschritte mit den Deprecation-Warnungen fehlen, die den Weg vereinfacht hätten.
Meine Empfehlung als Kadenz ist daher Minor-Upgrades monatlich als Routine mit automatischem Plan-Diff in der Pipeline. Major-Upgrades innerhalb von drei Monaten nach Release, geplant als eigener Change. Wer das nicht in den Betrieb bekommt, sollte es zumindest als wiederkehrenden Termin im Kalender haben, statt es dem Zufall eines Dependabot-PRs zu überlassen.
Wenn niemand mehr den Code versteht
Bis hierher war das ein technisches Thema.
Ein erheblicher Teil der Landing Zones, die ich übernehme, wurde extern gebaut. Ein Dienstleister hat vor zwei oder drei Jahren Module geschrieben, das Projekt abgeschlossen, das Wissen mitgenommen. Der Code läuft. Solange nichts angefasst wird, fällt niemandem auf, dass ihn niemand mehr vollständig versteht.
Ein Provider-Major macht diesen Zustand sichtbar. Plötzlich muss jemand beurteilen, ob ein geplanter Replace an einem Key Vault fachlich akzeptabel ist. Diese Beurteilung setzt voraus, dass man weiß, was in dem Key Vault liegt und wer darauf zugreift. Wenn diese Information nirgends dokumentiert ist, gibt es zwei Optionen: raten oder nichts tun. Beide sind schlecht.
Die operativen Warnsignale, an denen ich das erkenne:
Es existiert keine Non-Prod-Stage, die die Produktion strukturell abbildet. Die Provider-Version ist gar nicht oder nur als offene Untergrenze gepinnt. Die .terraform.lock.hcl liegt nicht im Repository. Im Repository gibt es seit Monaten keinen Commit, aber terraform plan zeigt Änderungen. Und niemand kann auf Anhieb sagen, welches Modul welche Ressourcen in welcher Subscription verwaltet.
Jedes einzelne dieser Signale ist für sich beherrschbar. Alle zusammen bedeuten, dass die Infrastruktur nur so lange stabil ist, wie sie nicht verändert wird. Das ist keine Infrastruktur, das ist ein tickende Zeitbombe.
Fazit
- Setzt vor allem anderen ein explizites Provider-Constraint und committet die Lockfile. Das kostet zehn Minuten und nimmt euch den unkontrollierten Major-Sprung aus der Pipeline.
- Wertet Pläne automatisiert aus. Jede Ressource mit
deletein den Plan-Aktionen ist ein eigener Hinweis darauf, dass sich das Jemand anschauen sollte. - Migriert in der Reihenfolge State-Backend, Governance, Netzwerk, Shared Services, Workloads. Ein Modul pro Change, Non-Prod vor Prod.
- Legt eine Upgrade-Kadenz fest. Minors monatlich und Majors innerhalb von drei Monaten. Pinning ohne Kadenz verschiebt die Aufgaben nur nach hinten und macht es nur schwerer.
Wenn ihr nicht sicher sagen könnt, wie euer nächster terraform plan gegen die Produktion aussieht, ist das dein eigentliches Problem. Genau das schaue ich mir im kostenfreien Azure Cloud Audit an. Dein IaC-Stand, Provider- und Modulversionen, State-Drift, Stage-Struktur und die konkreten Ressourcen, die beim nächsten Major-Upgrade gefährdet sind. Ergebnis ist ein priorisierter Maßnahmenplan mit Aufwandsschätzung. Wenn ihr wissen wollt, ob sich das für eure Umgebung lohnt, lass uns das gerne zusammen anschauen.
Weiterführende Quellen
- Terraform on Azure: Overview
- Store Terraform state in Azure Storage
- Migrate from vault access policy to an Azure RBAC permission model
- Configure Azure Storage firewalls and virtual networks
- Subnet delegation in Azure Virtual Network
- Use a managed identity in Azure Kubernetes Service
- Azure landing zone design areas (Cloud Adoption Framework)
- HashiCorp AzureRM Provider: Registry und Changelog
- Terraform: refactoring mit moved-Blöcken

