Vor Kurzem wurde ich auf formae aufmerksam gemacht, ein Open-Source-IaC-Tool von Platform Engineering Labs. formae möchte verändern, wie wir Infrastructure as Code betreiben, und implementiert dabei andere Ansätze und Paradigmen aus als Terraform oder Pulumi. Im Kern steht ein (nicht KI-basierter) Agent, der deine Umgebung verwaltet und kontinuierlich überwacht. Das ermöglicht automatische Resource Discovery, das heißt formae erkennt Infrastruktur auch dann, wenn sie per ClickOps oder von anderen IaC-Tools erstellt wurde. Da der Agent den tatsächlichen Zustand kontinuierlich beobachtet, entsteht das übliche Problem des State-Drifts erst gar nicht. Anders als bei traditionellen Tools, bei denen du Ressourcen erst manuell in eine State-Datei importieren musst, kann formae direkt den passenden Infrastrukturcode generieren, den du sofort nutzen kannst.
Beim Deployment unterstützt es sowohl klassische GitOps-Workflows als auch einen experimentellen No-Git-GitOps-Ansatz, bei dem das Tool selbst als System-of-Record fungiert. So haben Teams die Wahl zwischen strikter Kontrolle über Git und einem flexibleren Modell, in dem Git optional wird und Infrastruktur mit temporären Konfigurationsdateien verwaltet werden kann.
Für mehr Details deckt die Dokumentation viele Themen ab, von den Grundlagen bis zu CI/CD-Integration, Security und Networking.
Terraform vs. formae
Wenn du als Cloud-, DevOps- oder Platform-Engineer arbeitest, hast du mit ziemlicher Sicherheit deine Erfahrungen mit Terraform gemacht. Als De-facto-Standard ist die Community riesig, und es gibt einen Provider für so gut wie alles. Dennoch wette ich, dass du deine schmerzhaften Momente mit Terraform hattest:
- Bereits existierende, noch nicht von Terraform verwaltete Ressourcen importieren
- Drifts zwischen Terraform-Konfiguration und Infrastruktur
- State-Locks
- Umgang mit Remote-State
State-File vs. Live-Realität: In Terraform gilt die .tfstate-Datei als absolute Source-of-Truth. Wenn jemand eine Ressource per ClickOps, Skript oder anderem Tool anpasst, bleibt Terraform von diesen Änderungen komplett unwissend, bis eine manuelle State-Synchronisation erzwungen wird. Wenn Code und tatsächliche Infrastruktur auseinanderlaufen, wird das Beheben des Drifts zur komplexen Aufgabe. formae geht das anders an: dein Pkl-Code definiert, was formae aktiv verwaltet, während der Agent parallel dazu den Rest deiner Cloud beobachtet, jede Änderung außerhalb von formae registriert und in einen internen State versioniert. In der Praxis heißt das: du kannst formae in ein bestehendes Azure-Setup einführen, ohne vorher alles abzureißen, was Terraform, Skripte oder ClickOps hinterlassen haben.
Globale Stack-Updates vs. granulares Patching: Ein klassischer Terraform-Workflow wendet Infrastruktur-Änderungen ganzheitlich auf einen kompletten Stack an. Ein einziger Fehler beim Anlegen oder Ändern einer Ressource oder ein sich unerwartet verhaltender Provider reicht aus, um die gesamte Deployment-Pipeline zum Stillstand zu bringen und andere Ressourcen in Mitleidenschaft zu ziehen. formae verändert dieses Deployment-Modell, indem es Stack-Änderungen asynchron und pro Ressource isoliert ausführt. Es führt einen "Patch-Mode" ein, der es Nutzer:innen erlaubt, präzise, granulare Property-Updates oder Hotfixes direkt auf spezifische Ressourcen per Code anzuwenden, was den operativen Blast Radius deutlich verkleinert.
Statische DSL vs. statisch typisierte Konfiguration: Terraform setzt auf die HashiCorp Configuration Language (HCL). HCL ist zwar ausgereift, aber im Grunde eine Domain-Specific Language, die mit komplexer Logik schwer klarkommt und oft Workarounds oder verbose Template-Funktionen erfordert, um Bedingungen, Schleifen und tief verschachtelte Datenstrukturen abzubilden. formae nutzt stattdessen Apples Open-Source-Sprache Pkl. Pkl schließt die Lücke mit einer statisch typisierten Sprache, die speziell für Konfiguration entwickelt wurde. Sie bringt native Fähigkeiten wie Typsicherheit, Vererbung und eingebaute Validierungsregeln direkt in deine Infrastruktur-Dateien und fängt Fehler schon beim Evaluieren ab, bevor der Code je deinen Cloud-Provider erreicht. Platform Engineering Labs bietet hier eine 16-Schritte-Hands-on-Tour an.
Intro
Dieser Blogpost ist Teil der formae by Example-Serie, der komplette Code liegt in einem eigenen GitHub-Repository. Jeder Teil ist als eigenständiger Ordner organisiert, den du von oben nach unten lesen, evaluieren und direkt anwenden kannst. Während zukünftige Teile Remote-Agents, CI/CD-Integration und das Steuern von formae über einen MCP-Server behandeln werden, legt Teil 1 den Fokus auf die Grundlagen und führt dich Schritt für Schritt durch eine Terraform-zu-formae-Migration eines Azure-Stacks.
formae from scratch — Azure SQL hinter einem Private Endpoint
Voraussetzungen
formae installieren:
1/bin/bash -c "$(curl -fsSL https://hub.platform.engineering/get/formae.sh)"
Binary zum PATH hinzufügen:
1echo 'export PATH=/opt/pel/bin:$PATH' >> ~/.zshrc 2source ~/.zshrc
Für bash oder andere Shells siehe local install.
Verifizieren:
1formae --version
Azure CLI eingeloggt und die richtige Subscription ausgewählt:
1export AZURE_SUBSCRIPTION_ID=<your-sub-id> 2az login 3az account set --subscription $AZURE_SUBSCRIPTION_ID
Ein laufender formae Agent mit installiertem Plugin:
1formae plugin install azure@0.1.6 2formae agent start
Aufbau
Statt alle elf Ressourcen Zeile für Zeile durchzugehen, konzentrieren wir uns auf die strukturellen Highlights. (Wenn du die Syntax noch nicht kennst, lohnt sich vorher ein Blick in die offiziellen Fundamentals der formae-Dokumentation.). Wenn wir uns unsere main.pkl anschauen, ist die Datei in drei Hauptblöcke gegliedert: description, properties und forma.
Der erste Block ist der description-Block. Er hält die Metadaten deiner Konfiguration und dokumentiert sie gleichzeitig selbst.
1description {
2 text = "formae demo: Azure SQL with a User-Assigned MI as Entra-only admin, locked down behind a Private Endpoint — all chained via .res references."
3 confirm = true // Prompts user for confirmation before applying
4}
Jede im properties-Block definierte Property wird automatisch zu einem Kommandozeilen-Flag, sodass du dein Deployment anpassen kannst, ohne den Code selbst zu verändern.
1properties {
2 location = new formae.Prop {
3 flag = "location"
4 default = "westeurope"
5 }
6 instance = new formae.Prop {
7 flag = "instance"
8 default = "001"
9 }
10 dbSku = new formae.Prop {
11 flag = "db-sku"
12 default = "Basic"
13 }
14}
Alle verfügbaren Properties kannst du dynamisch anzeigen lassen mit:
1formae apply --help main.pkl
Output:
1[...]
2Properties:
3 --db-sku property: db-sku [default: "Basic"]
4 --instance property: instance [default: "001"]
5 --location property: location [default: "westeurope"]
6[...]
Im forma-Block wird die eigentliche Infrastruktur definiert:
1[...]
2forma {
3 // Define a stack to organize your resources
4 new formae.Stack {
5 label = vars.stackName
6 description = "Persistent stack for the demo"
7 }
8
9 // Define a target for where resources will be created
10 new formae.Target {
11 label = "azure-\(stackLocation)"
12 discoverable = true
13 config = new azure.Config {
14 subscriptionId = vars.subscriptionId
15 }
16 }
17
18 // Define your infrastructure resources
19 local rg = new resourcegroup.ResourceGroup {
20 label = "rg-\(suffix)"
21 name = label
22 location = stackLocation
23 }
24
25 rg
26[...]
In Pkl macht die Deklaration eines Objekts mit dem local-Modifier daraus eine private, nicht-renderbare Property. Es wird im Speicher evaluiert, aber nicht in die Output-Datei geschrieben, die formae an deinen Cloud-Provider übergibt. Um die Ressource tatsächlich in formaes Deployment-Manifest zu übernehmen, musst du sie außerhalb des lokalen Scopes referenzieren (die abschließende rg-Zeile oben).
Einmal definiert, kannst du sie in anderen Ressourcen referenzieren:
1local vnet = new virtualnetwork.VirtualNetwork {
2 label = "vnet-\(suffix)"
3 name = label
4 location = rg.location
5 resourceGroupName = rg.res.name
6 addressSpace = new virtualnetwork.AddressSpace {
7 addressPrefixes = new Listing { "10.10.0.0/16" }
8 }
9}
Beachte, dass Properties hier auf zwei unterschiedliche Arten referenziert werden: rg.location und rg.res.name.
rg.location:
Wenn du Konfigurationswerte weitergibst, die vollständig statisch sind (wie Locations oder Tags), z. B. aus einem Kommandozeilen-Flag, reicht das direkte Auslesen aus der lokalen Pkl-Objektreferenz vollkommen aus. formae kann den Wert sofort evaluieren.
rg.res.name:
Wenn du Ressourcen für Deployment-Abhängigkeiten miteinander verknüpfst, referenziere sie immer über .res (z. B. rg.res.name, snet.res.id). Damit signalisierst du der formae-Engine explizit, dass sie den State der Ressource vom Agent bzw. Cloud-Provider auflösen muss, bevor sie den finalen Execution Graph baut.
Eval und Apply
Jetzt, wo wir uns die main.pkl angeschaut haben, deployen wir ein paar Ressourcen in unsere Azure-Subscription: Einen Azure SQL Server, abgeschottet in einem privaten Netzwerk, mit einer User-Assigned Managed Identity als einzigen Admin.
Klone dieses Repository von GitHub: formae-example. Navigiere zu part-1-intro/01-from-scratch und evaluiere die Forma:
1formae eval main.pkl
Das zeigt dir die Ressourcen, die formae erstellen möchte, im JSON-Format. Jetzt führe aus:
1formae apply --mode reconcile --yes --status-output-layout detailed --watch main.pkl
Output:
1oooooo ooooo oooo oooo ooooo ooo oooo 2 0oo o0o0 oo0o o0o 0oo ooo0o 0o0 0o 00 0o 00o 3ooo 0oo o0 0o0 ooo ooo oo0 o0 0oo oo 4ooo oo0 oo0 oo oo oo oo o00 o0oo 5oooooo0 ooo oo oo oo oo oo o00 ooo 6ooo oo 0oo oo oo oo oo 00 o00 o0o 7ooo 0oo o0o oo oo oo oo o0 oooo0 ooo 8ooo o00000oo oo oo o0 oo 0000o 0000o v0.87.0 9 10Watching commands status (refreshing every 2s)... 11 12 13apply command with ID 3GDfZ25wicLzqVCVGxZkzXN2b0i: InProgress (total duration: 14s) 14├── create stack stack-formae-demo-dev: Success (duration: 3ms) 15│ └── description: Persistent stack for the demo; resources here are billed. 16├── create resource pdzg-sql-formae-demo-dev-001: NotStarted 17│ ├── of type AZURE::Network::PrivateDnsZoneGroup 18│ └── in stack stack-formae-demo-dev 19├── create resource rg-formae-demo-dev-001: Success 20│ ├── of type AZURE::Resources::ResourceGroup 21│ └── in stack stack-formae-demo-dev 22├── create resource snet-pe-formae-demo-dev-001: NotStarted 23│ ├── of type AZURE::Network::Subnet 24│ └── in stack stack-formae-demo-dev 25├── create resource pdzvnl-formae-demo-dev-001: NotStarted 26│ ├── of type AZURE::Network::PrivateDnsZoneVirtualNetworkLink 27│ └── in stack stack-formae-demo-dev 28├── create resource sqldb-appdb: NotStarted 29│ ├── of type AZURE::Sql::Database 30│ └── in stack stack-formae-demo-dev 31├── create resource sql-formae-demo-dev-001: InProgress 32│ ├── of type AZURE::Sql::Server 33│ ├── in stack stack-formae-demo-dev 34│ └── attempt: 1/10 35├── create resource ra-formae-demo-dev-001-mi-reader-rg: Success 36│ ├── of type AZURE::Authorization::RoleAssignment 37│ └── in stack stack-formae-demo-dev 38├── create resource id-formae-demo-dev-001: Success 39│ ├── of type AZURE::ManagedIdentity::UserAssignedIdentity 40│ └── in stack stack-formae-demo-dev 41├── create resource pdz-sql-formae-demo-dev-001: InProgress 42│ ├── of type AZURE::Network::PrivateDnsZone 43│ ├── in stack stack-formae-demo-dev 44│ └── attempt: 1/10 45├── create resource pe-sql-formae-demo-dev-001: NotStarted 46│ ├── of type AZURE::Network::PrivateEndpoint 47│ └── in stack stack-formae-demo-dev 48└── create resource vnet-formae-demo-dev-001: InProgress 49 ├── of type AZURE::Network::VirtualNetwork 50 ├── in stack stack-formae-demo-dev 51 └── attempt: 1/10
Nach ein paar Sekunden oder Minuten sind alle Ressourcen erstellt:
Prüfe, welche Ressourcen von formae verwaltet werden mit managed:true:
1formae inventory resources --query="managed:true stack:stack-formae-demo-dev" --max-results 50
Output:
1oooooo ooooo oooo oooo ooooo ooo oooo 2 0oo o0o0 oo0o o0o 0oo ooo0o 0o0 0o 00 0o 00o 3ooo 0oo o0 0o0 ooo ooo oo0 o0 0oo oo 4ooo oo0 oo0 oo oo oo oo o00 o0oo 5oooooo0 ooo oo oo oo oo oo o00 ooo 6ooo oo 0oo oo oo oo oo 00 o00 o0o 7ooo 0oo o0o oo oo oo oo o0 oooo0 ooo 8ooo o00000oo oo oo o0 oo 0000o 0000o v0.87.0 9 10┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────┬──────────────────────────────────────────────────┬─────────────────────────────────────┐ 11│ NativeID │ Stack │ Type │ Label │ 12├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────┤ 13│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-formae-demo-dev-001/providers/Microsoft.Authorization/roleAssignments/8e332f19-e3a9-482f-b4fc-8bd4d4dacde6 │ stack-formae-demo-dev │ AZURE::Authorization::RoleAssignment │ ra-formae-demo-dev-001-mi-reader-rg │ 14├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────┤ 15│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourcegroups/rg-formae-demo-dev-001/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id-formae-demo-dev-001 │ stack-formae-demo-dev │ AZURE::ManagedIdentity::UserAssignedIdentity │ id-formae-demo-dev-001 │ 16├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────┤ 17│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-formae-demo-dev-001/providers/Microsoft.Network/privateDnsZones/privatelink.database.windows.net │ stack-formae-demo-dev │ AZURE::Network::PrivateDnsZone │ pdz-sql-formae-demo-dev-001 │ 18├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────┤ 19│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-formae-demo-dev-001/providers/Microsoft.Network/privateEndpoints/pe-sql-formae-demo-dev-001/privateDnsZoneGroups/default │ stack-formae-demo-dev │ AZURE::Network::PrivateDnsZoneGroup │ pdzg-sql-formae-demo-dev-001 │ 20├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────┤ 21│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-formae-demo-dev-001/providers/Microsoft.Network/privateDnsZones/privatelink.database.windows.net/virtualNetworkLinks/link-formae-demo-dev-001 │ stack-formae-demo-dev │ AZURE::Network::PrivateDnsZoneVirtualNetworkLink │ pdzvnl-formae-demo-dev-001 │ 22├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────┤ 23│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-formae-demo-dev-001/providers/Microsoft.Network/privateEndpoints/pe-sql-formae-demo-dev-001 │ stack-formae-demo-dev │ AZURE::Network::PrivateEndpoint │ pe-sql-formae-demo-dev-001 │ 24├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────┤ 25│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-formae-demo-dev-001/providers/Microsoft.Network/virtualNetworks/vnet-formae-demo-dev-001/subnets/snet-pe-formae-demo-dev-001 │ stack-formae-demo-dev │ AZURE::Network::Subnet │ snet-pe-formae-demo-dev-001 │ 26├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────┤ 27│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-formae-demo-dev-001/providers/Microsoft.Network/virtualNetworks/vnet-formae-demo-dev-001 │ stack-formae-demo-dev │ AZURE::Network::VirtualNetwork │ vnet-formae-demo-dev-001 │ 28├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────┤ 29│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-formae-demo-dev-001 │ stack-formae-demo-dev │ AZURE::Resources::ResourceGroup │ rg-formae-demo-dev-001 │ 30├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────┤ 31│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-formae-demo-dev-001/providers/Microsoft.Sql/servers/sql-formae-demo-dev-001/databases/appdb │ stack-formae-demo-dev │ AZURE::Sql::Database │ sqldb-appdb │ 32├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────┤ 33│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-formae-demo-dev-001/providers/Microsoft.Sql/servers/sql-formae-demo-dev-001 │ stack-formae-demo-dev │ AZURE::Sql::Server │ sql-formae-demo-dev-001 │ 34└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴───────────────────────┴──────────────────────────────────────────────────┴─────────────────────────────────────┘ 35 36Summary: Showing 11 of 11 total resources
Reconcile- und Patch-Mode
Es gibt zwei Apply-Modi: reconcile und patch. Aus der formae-Dokumentation:
Reconcile-Mode (--mode reconcile, Default): Der gewünschte Zustand ist die vollständige Source-of-Truth. Elemente, die nicht in deiner Forma-Datei stehen, werden entfernt.
Patch-Mode (--mode patch): Kann anlegen und aktualisieren, löscht aber nie etwas. Wenn eine Ressource in deiner Cloud existiert, aber in deiner main.pkl komplett fehlt, ignoriert der Patch-Mode sie.
| Verhalten | --mode reconcile | --mode patch |
|---|---|---|
| Philosophie | Die Datei ist die absolute Wahrheit. | Hinzufügen ohne zu entfernen. |
| Fehlende Ressource | Wird in der Cloud zerstört. | Bleibt bestehen |
| Out-of-Band-Tags | Werden entfernt. | Bleiben erhalten. |
| Drift-Sicherheit | Schlägt fehl, wenn die Cloud seit dem letzten Apply gedriftet ist. Mit dem --force-Flag lassen sich alle Out-of-Band-Änderungen rückgängig machen, ohne dass der Apply fehlschlägt. | Schlägt bei Drift nie fehl, append-only Merge |
Du hast die formae-Konfiguration vorhin mit dem Reconcile-Mode angewendet, jetzt testen wir den Patch-Mode:
Füge eine weitere Datenbank hinzu:
1new sqldatabase.Database {
2 label = "sqldb-logdb"
3 name = "logdb"
4 location = rg.location
5 resourceGroupName = rg.res.name
6 serverName = sqlServer.res.name
7 sku = new sqldatabase.SKU {
8 name = dbSkuName
9 tier = dbSkuTier
10 }
11}
Nutze diesmal den Patch-Mode:
1formae apply --mode patch --yes --status-output-layout detailed --watch main.pkl
Output:
1oooooo ooooo oooo oooo ooooo ooo oooo 2 0oo o0o0 oo0o o0o 0oo ooo0o 0o0 0o 00 0o 00o 3ooo 0oo o0 0o0 ooo ooo oo0 o0 0oo oo 4ooo oo0 oo0 oo oo oo oo o00 o0oo 5oooooo0 ooo oo oo oo oo oo o00 ooo 6ooo oo 0oo oo oo oo oo 00 o00 o0o 7ooo 0oo o0o oo oo oo oo o0 oooo0 ooo 8ooo o00000oo oo oo o0 oo 0000o 0000o v0.87.0 9 10Watching commands status (refreshing every 2s)... 11 12 13apply command with ID 3GDiwoMvpbBheOPyLZvZ1UUEwkn: Success (total duration: 48s) 14└── create resource sqldb-logdb: Success (duration: 48s) 15 ├── of type AZURE::Sql::Database 16 └── in stack stack-formae-demo-dev
Der Output zeigt, dass nur die neue Datenbank erstellt wird. Nachdem du sie aus main.pkl entfernst und erneut mit --mode patch ausführst, passiert nichts:
Output:
1oooooo ooooo oooo oooo ooooo ooo oooo 2 0oo o0o0 oo0o o0o 0oo ooo0o 0o0 0o 00 0o 00o 3ooo 0oo o0 0o0 ooo ooo oo0 o0 0oo oo 4ooo oo0 oo0 oo oo oo oo o00 o0oo 5oooooo0 ooo oo oo oo oo oo o00 ooo 6ooo oo 0oo oo oo oo oo 00 o00 o0o 7ooo 0oo o0o oo oo oo oo o0 oooo0 ooo 8ooo o00000oo oo oo o0 oo 0000o 0000o v0.87.0 9 10No changes needed: 11 12The specified forma resources are up to date.
Wechselst du zurück auf --mode reconcile, wird der Apply abgelehnt: Der Patch-Lauf hat einen Drift gegenüber dem Stand erzeugt, den Reconcile zuletzt kannte, und Reconcile will nichts stillschweigend löschen, was es nicht selbst kennt.
1oooooo ooooo oooo oooo ooooo ooo oooo 2 0oo o0o0 oo0o o0o 0oo ooo0o 0o0 0o 00 0o 00o 3ooo 0oo o0 0o0 ooo ooo oo0 o0 0oo oo 4ooo oo0 oo0 oo oo oo oo o00 o0oo 5oooooo0 ooo oo oo oo oo oo o00 ooo 6ooo oo 0oo oo oo oo oo 00 o00 o0o 7ooo 0oo o0o oo oo oo oo o0 oooo0 ooo 8ooo o00000oo oo oo o0 oo 0000o 0000o v0.87.0 9 10Error: forma rejected because the stacks it references have been modified since the last reconcile command. 11 12There are two options to resolve this issue: 13 1) use the '--force' flag to apply the forma anyway (this will overwrite any changes made since the last reconcile), or 14 2) manually adjust your own code: 15 - extract the changes made since the last reconcile and incorporate them in your forma before applying it again. 16 17 Here is the list of extract commands to use (use different target file names): 18 19 formae extract --query='stack:stack-formae-demo-dev type:AZURE::Sql::Database label:sqldb-logdb' <target forma file>
Du kannst entweder erneut mit --force ausführen (Reconcile gewinnt, logdb wird gelöscht) oder formae extract ausführen, um logdb in eine Forma-Datei zu ziehen. Das schauen wir uns später noch an.
Bevor wir zum nächsten Kapitel (02-terraform-original) kommen, entfernen wir die Ressourcen:
1formae destroy --query 'stack:stack-formae-demo-dev' --watch
Das Terraform-Äquivalent — Azure SQL hinter einem Private Endpoint
Bevor wir uns anschauen, wie man bestehende Ressourcen unter formaes Kontrolle bringt, brauchen wir tatsächlich erst mal Infrastruktur, mit der wir arbeiten können. Statt im Azure Portal herumzuklicken, um das manuell aufzubauen, provisionieren wir unsere Ausgangsumgebung mit Terraform.
Das kommt einem gängigen realen Szenario nahe. Da Terraform in der Branche weit verbreitet ist, beginnt die Einführung eines neuen Tools wie formae oft mit der Migration von oder dem Anschluss an Infrastruktur, die ursprünglich mit einem klassischen Terraform-Setup provisioniert wurde. Wir gehen hier nicht ins Detail.
Navigiere zu part-1-intro/02-terraform-original und wende die Terraform-Konfiguration an:
Sie enthält den gleichen Ressourcen-Stack, den wir vorher mit formae erstellt haben.
Setze zuerst deine subscription_id:
1export TF_VAR_subscription_id=<your-sub-id>
Initialisieren:
1terraform init
Anwenden:
1terraform apply -auto-approve
Nach ein paar Sekunden oder Minuten sind alle Ressourcen erstellt:
Entferne diese Ressourcen noch nicht, wir brauchen sie im nächsten Kapitel.
Die Migration — formae die Kontrolle über die Infratruktur übergeben
Jetzt, wo unsere Infrastruktur über Terraform läuft, stehen wir vor einer klassischen Day-2-Operations-Herausforderung: Wie übergeben wir die Kontrolle an formae, ohne Ressourcen zu entfernen und Downtime zu verursachen?
In einem traditionellen Workflow erfordert die Einführung eines neuen IaC-Tools meist einen mühsamen Import-Prozess, bei dem Ressourcen-IDs einzeln in eine neue State-Datei gemappt werden. formae geht das anders an, indem es den Fokus von statischen State-Files auf aktives Management verlagert.
Schauen wir uns an, wie formae diese Ressourcen entdeckt und die Kontrolle darüber sauber übernimmt.
1. Resource Discovery
formae findet aktiv, was in deiner Subscription liegt. Der formae Agent scannt deine Cloud-Targets in einem festen Intervall (standardmäßig alle 5 Minuten). Da wir unsere Ausgangsinfrastruktur im vorherigen Kapitel deployt haben, bestehen diese Ressourcen bereits in Azure und werden vom Agent als unmanaged kategorisiert.
Diese unmanaged Ressourcen kannst du direkt aus dem Terminal mit Wildcards abfragen:
1formae inventory resources --query="managed:false label:*tf-demo-dev*"
Output:
1oooooo ooooo oooo oooo ooooo ooo oooo 2 0oo o0o0 oo0o o0o 0oo ooo0o 0o0 0o 00 0o 00o 3ooo 0oo o0 0o0 ooo ooo oo0 o0 0oo oo 4ooo oo0 oo0 oo oo oo oo o00 o0oo 5oooooo0 ooo oo oo oo oo oo o00 ooo 6ooo oo 0oo oo oo oo oo 00 o00 o0o 7ooo 0oo o0o oo oo oo oo o0 oooo0 ooo 8ooo o00000oo oo oo o0 oo 0000o 0000o v0.87.0 9 10┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬───────────┬──────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────┐ 11│ NativeID │ Stack │ Type │ Label │ 12├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 13│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourcegroups/rg-tf-demo-dev-001/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id-tf-demo-dev-001 │ unmanaged │ AZURE::ManagedIdentity::UserAssignedIdentity │ id-tf-demo-dev-2 │ 14├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 15│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Network/networkInterfaces/pe-sql-tf-demo-dev-001.nic.a76b241e-c45f-4939-817f-8b7fd195305c │ unmanaged │ AZURE::Network::NetworkInterface │ pe-sql-tf-demo-dev-001.nic.a76b241e-c45f-4939-817f-8b7fd195305c │ 16├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 17│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Network/privateEndpoints/pe-sql-tf-demo-dev-001/privateDnsZoneGroups/default │ unmanaged │ AZURE::Network::PrivateDnsZoneGroup │ rg-tf-demo-dev-001-default-6 │ 18├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 19│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Network/privateDnsZones/privatelink.database.windows.net/virtualNetworkLinks/link-tf-demo-dev-001 │ unmanaged │ AZURE::Network::PrivateDnsZoneVirtualNetworkLink │ link-tf-demo-dev-2 │ 20├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 21│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Network/privateEndpoints/pe-sql-tf-demo-dev-001 │ unmanaged │ AZURE::Network::PrivateEndpoint │ rg-tf-demo-dev-001-pe-sql-tf-demo-dev-2 │ 22├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 23│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Network/virtualNetworks/vnet-tf-demo-dev-001/subnets/snet-pe-tf-demo-dev-001 │ unmanaged │ AZURE::Network::Subnet │ snet-pe-tf-demo-dev-2 │ 24├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 25│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Network/virtualNetworks/vnet-tf-demo-dev-001 │ unmanaged │ AZURE::Network::VirtualNetwork │ vnet-tf-demo-dev-2 │ 26├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 27│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001 │ unmanaged │ AZURE::Resources::ResourceGroup │ rg-tf-demo-dev-001-pe-sql-tf-demo-dev-2 │ 28├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 29│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Sql/servers/sql-tf-demo-dev-001/databases/appdb │ unmanaged │ AZURE::Sql::Database │ rg-tf-demo-dev-001-appdb-6 │ 30├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 31│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Sql/servers/sql-tf-demo-dev-001/databases/master │ unmanaged │ AZURE::Sql::Database │ rg-tf-demo-dev-001-master-6 │ 32├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 33│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Sql/servers/sql-tf-demo-dev-001 │ unmanaged │ AZURE::Sql::Server │ sql-tf-demo-dev-2 │ 34└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴───────────┴──────────────────────────────────────────────────┴─────────────────────────────────────────────────────────────────┘ 35 36Summary: Showing 11 of 11 total resources
Ohne das Label würde er alle Ressourcen in dieser Azure-Subscription anzeigen, die nicht von formae verwaltet werden.
Beim Ausführen unserer initialen Wildcard-Inventory-Abfrage (label:*tf-demo-dev*) fällt auf, dass formae 11 Ressourcen zurückgibt. Unsere Architektur besteht aber eigentlich aus 13 Teilen. Zwei entscheidende Ressourcen fehlen in dieser ersten Liste: die Private DNS Zone und das Role Assignment.
Warum hat die Wildcard sie nicht erfasst? Das liegt daran, wie Azure Ressourcen benennt:
- Die Private DNS Zone: Damit sie mit Azure SQL korrekt funktioniert, muss die Zone global exakt
privatelink.database.windows.netheißen. Sie kann keinen eigenen Workload-Präfix oder Instance-Suffix enthalten. - Das Role Assignment: Azure generiert Role Assignments mit einer komplett zufälligen, eindeutigen GUID (z. B.
26ed4d89-2633-f4a6-37b6-...) als Ressourcen-Namen.
Da keiner dieser Ressourcen-Namen den String tf-demo-dev enthält, überspringt formaes Wildcard-Filter sie. Um diese Ausreißer zu finden und einzusammeln, damit wir sie gemeinsam mit dem Hauptstack migrieren können, fragen wir sie einfach über ihre expliziten Azure-Ressourcen-Typen ab:
1formae inventory resources --query="managed:false type:AZURE::Network::PrivateDnsZone"
1formae inventory resources --query="managed:false type:AZURE::Authorization::RoleAssignment"
Wenn deine Azure-Subscription viele aktive RBAC-Zuweisungen hat, kann der zweite Befehl eine lange Liste zurückgeben. Du findest die gesuchte Ressource, indem du die NativeID-Spalte im Output nach der Zeile durchsuchst, die explizit deine Resource Group (rg-tf-demo-dev-001) nennt. Kopiere die eindeutige GUID-Label, du brauchst sie später im Extract-Befehl (siehe Abschnitt 2. b).
2. Der Migration-Workflow:
a. Extract and Adopt
Statt Pkl-Code von Hand zu schreiben, um dem nachzubauen, was Terraform erstellt hat, lässt du formae das für dich erledigen.
1formae extract --query="managed:false label:*tf-demo-dev*" ./discovered.pkl
Das erzeugt eine discovered.pkl-Datei, die deine realen Ressourcen abbildet. Um diese Ressourcen von unmanaged in einen vollständig managed-Zustand zu überführen, musst du nur eine Sache in der extrahierten Datei anpassen: das Stack-Label.
1local myStack = new formae.Stack {
2 label = "stack-tf-migration-dev" // Assigning a label triggers the adoption
3 description = "Imported from our Terraform baseline"
4}
⚠️ Der master database Gotcha
Azure legt unter jedem logischen SQL Server automatisch eine master database an. formae extract greift sie mit auf und erstellt einen Block dafür. Diesen Block mit zu übernehmen ist unkritisch, aber sobald du eine Version der Datei reconcilest, die ihn nicht mehr enthält, versucht formae ihn zu löschen und Azure lehnt mit CannotUseReservedDatabaseName: ab
1Cannot use reserved database name 'master' in this operation.
Der Server kann nicht gelöscht werden und das Apply schlägt fehl. Das ist keine formae-Einschränkung. Führe
1az sql db delete -n master -s sql-tf-demo-dev-001 -g rg-tf-demo-dev-001
selbst aus, und Azure liefert denselben Fehler, master ist reserviert und kann überhaupt nicht gelöscht werden. Der einfachste Workaround ist, den master-Block vor dem ersten Apply aus deiner extrahierten Pkl zu entfernen und zu akzeptieren, dass formae inventory resources --query="managed:false" immer eine unmanaged master pro SQL Server anzeigt.
Nachdem du das master-database-Objekt entfernt hast, kannst du dies anwenden, um die Ownership dieser 10 Ressourcen sicher an formae zu übergeben.
1formae apply --mode reconcile --yes --watch --status-output-layout detailed discovered.pkl
Verifiziere jetzt, dass die Ressourcen von formae verwaltet werden:
1formae inventory resources --query="stack:stack-tf-migration-dev" --max-results 20
Output:
1oooooo ooooo oooo oooo ooooo ooo oooo 2 0oo o0o0 oo0o o0o 0oo ooo0o 0o0 0o 00 0o 00o 3ooo 0oo o0 0o0 ooo ooo oo0 o0 0oo oo 4ooo oo0 oo0 oo oo oo oo o00 o0oo 5oooooo0 ooo oo oo oo oo oo o00 ooo 6ooo oo 0oo oo oo oo oo 00 o00 o0o 7ooo 0oo o0o oo oo oo oo o0 oooo0 ooo 8ooo o00000oo oo oo o0 oo 0000o 0000o v0.87.0 9 10┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬────────────────────────┬──────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────┐ 11│ NativeID │ Stack │ Type │ Label │ 12├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 13│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourcegroups/rg-tf-demo-dev-001/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id-tf-demo-dev-001 │ stack-tf-migration-dev │ AZURE::ManagedIdentity::UserAssignedIdentity │ id-tf-demo-dev-2 │ 14├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 15│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Network/networkInterfaces/pe-sql-tf-demo-dev-001.nic.a76b241e-c45f-4939-817f-8b7fd195305c │ stack-tf-migration-dev │ AZURE::Network::NetworkInterface │ pe-sql-tf-demo-dev-001.nic.a76b241e-c45f-4939-817f-8b7fd195305c │ 16├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 17│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Network/privateEndpoints/pe-sql-tf-demo-dev-001/privateDnsZoneGroups/default │ stack-tf-migration-dev │ AZURE::Network::PrivateDnsZoneGroup │ rg-tf-demo-dev-001-default-6 │ 18├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 19│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Network/privateDnsZones/privatelink.database.windows.net/virtualNetworkLinks/link-tf-demo-dev-001 │ stack-tf-migration-dev │ AZURE::Network::PrivateDnsZoneVirtualNetworkLink │ link-tf-demo-dev-2 │ 20├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 21│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Network/privateEndpoints/pe-sql-tf-demo-dev-001 │ stack-tf-migration-dev │ AZURE::Network::PrivateEndpoint │ rg-tf-demo-dev-001-pe-sql-tf-demo-dev-2 │ 22├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 23│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Network/virtualNetworks/vnet-tf-demo-dev-001/subnets/snet-pe-tf-demo-dev-001 │ stack-tf-migration-dev │ AZURE::Network::Subnet │ snet-pe-tf-demo-dev-2 │ 24├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 25│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Network/virtualNetworks/vnet-tf-demo-dev-001 │ stack-tf-migration-dev │ AZURE::Network::VirtualNetwork │ vnet-tf-demo-dev-2 │ 26├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 27│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001 │ stack-tf-migration-dev │ AZURE::Resources::ResourceGroup │ rg-tf-demo-dev-001-pe-sql-tf-demo-dev-2 │ 28├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 29│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Sql/servers/sql-tf-demo-dev-001/databases/appdb │ stack-tf-migration-dev │ AZURE::Sql::Database │ rg-tf-demo-dev-001-appdb-6 │ 30├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤ 31│ /subscriptions/7edc05f9-f6d6-451d-a516-8a6d4160986a/resourceGroups/rg-tf-demo-dev-001/providers/Microsoft.Sql/servers/sql-tf-demo-dev-001 │ stack-tf-migration-dev │ AZURE::Sql::Server │ sql-tf-demo-dev-2 │ 32└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────────────────────┴──────────────────────────────────────────────────┴─────────────────────────────────────────────────────────────────┘ 33 34Summary: Showing 10 of 10 total resources
b. Die zwei Ausreißer adoptieren
Wir haben im vorherigen Abschnitt gesehen, dass wir die Private DNS Zone und das Role Assignment separat extrahieren müssen.
1formae extract --query="managed:false type:AZURE::Network::PrivateDnsZone" ./dns.pkl
1formae extract --query="managed:false label:<ROLE_GUID_COPIED_FROM_BEFORE>" ./ra.pkl
Kopiere die PrivateDnsZone- und RoleAssignment-Blöcke aus diesen Dateien in discovered.pkl (und ergänze fehlende Import-Zeilen am Anfang). Wende erneut mit --watch an:
1formae apply --mode reconcile --yes --watch --status-output-layout detailed discovered.pkl
Output:
1oooooo ooooo oooo oooo ooooo ooo oooo 2 0oo o0o0 oo0o o0o 0oo ooo0o 0o0 0o 00 0o 00o 3ooo 0oo o0 0o0 ooo ooo oo0 o0 0oo oo 4ooo oo0 oo0 oo oo oo oo o00 o0oo 5oooooo0 ooo oo oo oo oo oo o00 ooo 6ooo oo 0oo oo oo oo oo 00 o00 o0o 7ooo 0oo o0o oo oo oo oo o0 oooo0 ooo 8ooo o00000oo oo oo o0 oo 0000o 0000o v0.87.0 9 10Watching commands status (refreshing every 2s)... 11 12 13apply command with ID 3GGmK0dLFPEKjkWmVlra3Xfhtuk: Success (total duration: 3s) 14├── update resource privatelink.database.windows.net-7: Success (duration: 3s) 15│ ├── of type AZURE::Network::PrivateDnsZone 16│ └── from unmanaged to stack-tf-migration-dev 17└── update resource 8b453e63-c139-fd96-ca9e-ee96ff9e1b6c: Success (duration: 2s) 18 ├── of type AZURE::Authorization::RoleAssignment 19 └── from unmanaged to stack-tf-migration-dev
Jetzt zeigt der inventory-Befehl 12 Ressourcen:
1formae inventory resources --query="stack:stack-tf-migration-dev"
Das war's, jetzt stehen die aus Terraform deployten Ressourcen unter formaes Verwaltung.
3. Drift erkennen und behandeln
Was passiert, wenn die tatsächliche Infrastruktur von deinem Code abweicht? Um zu zeigen, wie konsequent formae deinen Infrastruktur-State schützt, enthält das Github-Repository zwei Drift-Szenarien: einen Tag direkt über das Azure Portal mutieren oder die Werte zurück in deinem ursprünglichen Terraform-Verzeichnis ändern.
Dieses Designkonzept nennt das formae-Ökosystem "Co-Existence". Es ignoriert Änderungen anderer Tools wie Terraform nicht blind, sondern beobachtet aktiv dieselbe Cloud-Umgebung und macht Abweichungen sichtbar, damit du sie zu deinen eigenen Bedingungen auflösen kannst.
Um formaes Drift-Detection in Aktion zu sehen, simulieren wir ein klassisches Real-World-Problem: Out-of-Band-Änderungen. Stell dir vor, ein Kollege loggt sich ins Azure Portal ein (oder führt einen az-CLI-Befehl aus) und modifiziert manuell ein Resource-Group-Tag, ohne den Infrastruktur-Code zu aktualisieren:
1az group update -n rg-tf-demo-dev-001 --tags drift_test=v1 owner=alice
Sobald das passiert, ist der Live-State deiner Cloud von deinem Code abgewichen. Weil der formae Agent deine Umgebung kontinuierlich scannt, greift er dieses Delta bei seiner nächsten Sync-Runde auf (Default ist alle 5 Minuten).
Wenn du jetzt versuchst, ein Standard-Deployment auszuführen:
1formae apply --mode reconcile --yes discovered.pkl
Schützt formae aktiv deinen Stack. Anstatt blind loszulaufen oder den State zu überschreiben, verweigert der Agent die Ausführung und lehnt den Befehl mit einer klaren Warnung ab:
1oooooo ooooo oooo oooo ooooo ooo oooo 2 0oo o0o0 oo0o o0o 0oo ooo0o 0o0 0o 00 0o 00o 3ooo 0oo o0 0o0 ooo ooo oo0 o0 0oo oo 4ooo oo0 oo0 oo oo oo oo o00 o0oo 5oooooo0 ooo oo oo oo oo oo o00 ooo 6ooo oo 0oo oo oo oo oo 00 o00 o0o 7ooo 0oo o0o oo oo oo oo o0 oooo0 ooo 8ooo o00000oo oo oo o0 oo 0000o 0000o v0.87.0 9 10Error: forma rejected because the stacks it references have been modified since the last reconcile command. 11 12There are two options to resolve this issue: 13 1) use the '--force' flag to apply the forma anyway (this will overwrite any changes made since the last reconcile), or 14 2) manually adjust your own code: 15 - extract the changes made since the last reconcile and incorporate them in your forma before applying it again. 16 17 Here is the list of extract commands to use (use different target file names): 18 19 formae extract --query='stack:stack-tf-migration-dev type:AZURE::Resources::ResourceGroup label:rg-tf-demo-dev-001-pe-sql-tf-demo-dev-2' <target forma file>
Wenn Drift im reconcile-Mode auftritt, macht formae die exakte Ressource sichtbar, die abgewichen ist, und gibt dir zwei Möglichkeiten, um die Situation aufzulösen.
Weg A: Den Drift verwerfen (Pkl gewinnt)
Wenn die manuelle Änderung ein Fehler oder eine unautorisierte Modifikation war, kannst du deine Pkl-Datei als absolute Source-of-Truth durchsetzen. Indem du das --force-Flag anhängst, weist du den formae Agent an, den Drift bewusst zu überschreiben und Azure zurück auf den in deinem Code definierten Zustand zu bringen:
1formae apply --mode reconcile --yes --force --watch --status-output-layout detailed discovered.pkl
Das Ergebnis: Der Agent überschreibt den Drift und entfernt das Out-of-Band-Tag owner=alice aus der Azure Resource Group.
Weg B: Den Drift akzeptieren (Cloud gewinnt)
Wenn die manuelle Änderung beabsichtigt war (zum Beispiel ein Hotfix), kannst du sie in deine Codebase aufnehmen. formae zeigt dir den exakten formae extract-Befehl, den du brauchst.
1formae extract --query='stack:stack-tf-migration-dev type:AZURE::Resources::ResourceGroup label:rg-tf-demo-dev-001-pe-sql-tf-demo-dev-2' ./drift.pkl
Du kopierst einfach diesen aktualisierten tags-Block, fügst ihn zurück in deine discovered.pkl-Datei ein und startest dein Apply erneut. Der Reconcile läuft sauber durch, weil Code und Cloud wieder übereinstimmen.
Aufräumen:
1formae destroy --query 'stack:stack-tf-migration-dev' --watch
Fazit und Ausblick
Teil 1 deckt bewusst nur einen kleinen Ausschnitt ab: einen Azure-Stack, einen Localhost-Agent, eine Maschine. Selbst in diesem Rahmen zeigt sich der Unterschied zu einem Terraform-Workflow deutlich genug, um ihn hervorzuheben.
Was formae hier tatsächlich verändert hat
- Ressourcen unter Formae-Verwaltung bringen: eine Query, kein Import-Skript.
formae extracthat in Sekunden eine funktionierende Pkl-Datei für zehn Ressourcen produziert. Das Äquivalent in Terraform ist einterraform importpro Ressource plus die HCL von Hand nachschreiben und hoffen, dass die Resource Addresses beim ersten Anlauf sitzen. - Drift-Visibility in Aktion. Der Agent bemerkt die manuelle Änderung, bevor du es tust, und
applybricht ab, statt sie stillschweigend zu überschreiben. Du bekommst zwei Möglichkeiten: Pkl gewinnt (--force) oder Cloud gewinnt (extract + merge), statt dass das nächste Apply die manuelle Änderung einfach kommentarlos abräumt. - Patch-Mode ist ein vollwertiges Feature, kein Workaround. Eine Datenbank hinzuzufügen, ohne den restlichen Stack anzufassen, und dabei die Tags eines Monitoring-Teams beim nächsten Reconcile zu erhalten, ist in einem Modell, in dem jeder Apply den kompletten Stack durchgeht, wirklich schwer.
Was Teil 1 dir nicht erzählt
- Der Agent lief auf deinem Laptop. Was passiert, wenn der Agent auf Azure Container Instances läuft, wenn drei Engineers sich einen Stack teilen oder wenn eine CI-Pipeline nach einem Merge automatisch
applytriggert, ist hier nicht behandelt. - Die Demo hat 13 Ressourcen. Wie sich Extract, Apply und Drift-Detection-Latenz bei 500 oder 5.000 Ressourcen verhalten, ist ein anderes Thema.
- Der eigentliche Payoff von Pkl: Typsicherheit, Wiederverwendung von Modulen, Config-Fehler erkennen, bevor sie in Azure ankommen.
- Kleineres Ökosystem. Terraform hat für alles einen Provider, formae unterstützt heute nur einen Bruchteil davon. Schau dir die Azure supported resources an.
Wenn du formae evaluierst, entscheidet nicht die From-Scratch-Demo, sondern das Drift-Szenario in Abschnitt 3. Provoziere eine out-of-band-Änderung und schau, ob dein Team mit dem Workflow leben kann, wenn Cloud und Code auseinanderlaufen. Wenn ja, folgt der Rest. formae einzuführen erfordert kein komplettes Big-Bang-Rewrite deiner bestehenden Infrastruktur. Dank seines Co-Existence-Modells kannst du deine stabilen Terraform-Pipelines exakt so weiterlaufen lassen, wie sie sind, und formae gezielt für einzelne Teile deiner Infrastruktur einführen. Dieser inkrementelle Ansatz hält das Adoption-Risiko minimal. Der Tradeoff ist, dass du dann zwei Control Planes betreibst. Die Ownership-Grenzen zwischen dem, was Terraform verwaltet, und dem, was formae verwaltet, müssen explizit sein, sonst blockieren sich die beiden Tools gegenseitig und erzeugen genau die Drift-Probleme neu, die du eigentlich lösen wolltest. Wenn deine bestehenden Terraform-Pipelines stabil sind und du heute keinen Ärger mit deinem State oder Drift hast, ist formae für dich wahrscheinlich noch keine Priorität. Es liefert seinen echten Wert, wenn du komplexe Migrationen oder Umgebungen betreust, in denen mehrere Teams und Automations-Tools sicher koexistieren müssen.
Ausblick auf die nächsten Teile
| Teil | Fokus | Problem, das er adressiert |
|---|---|---|
| Teil 2 | Remote Agent auf ACI, CI/CD Integration, Observability | Verwandelt die Laptop-Demo in einen skalierbaren Team-Workflow. formaes aktives Agent-Modell umgeht die traditionellen Komplexitäten von Remote-Backend-Konfigurationen und State-Synchronisation komplett |
| Teil 3 | formae über einen MCP-Server steuern | Wie sieht "einen LLM einen Stack besitzen lassen" aus, wenn das Tool schon weiß, wie man "Realität ist gedriftet, Mensch entscheidet" sagt? |
Folge dem Repo unter waldemarschmalz/formae-example. Jeder Teil ist als eigenständiger Ordner verpackt.
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Blog-Autor*in
Waldemar Schmalz
Senior IT Consultant
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.