Beliebte Suchanfragen
//

formae Teil 1 – Schluss mit dem Kampf gegen dein State-File

24.7.2026 | 23 Minuten Lesezeit

Developer working in a boxing ring against Terraform, with Terraform shown as a chaotic hydra of cloud infrastructure heads.

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: formae resources in Azure Portal

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
PhilosophieDie Datei ist die absolute Wahrheit.Hinzufügen ohne zu entfernen.
Fehlende RessourceWird in der Cloud zerstört.Bleibt bestehen
Out-of-Band-TagsWerden entfernt.Bleiben erhalten.
Drift-SicherheitSchlä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: terraform resources in Azure Portal

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.net heiß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 extract hat in Sekunden eine funktionierende Pkl-Datei für zehn Ressourcen produziert. Das Äquivalent in Terraform ist ein terraform import pro 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 apply bricht 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.

Developer celebrating victory over Terraform with formae, symbolizing simplified infrastructure and state management.

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 apply triggert, 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

TeilFokusProblem, das er adressiert
Teil 2Remote Agent auf ACI, CI/CD Integration, ObservabilityVerwandelt 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 3formae über einen MCP-Server steuernWie 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.

Beitrag teilen

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//
Jetzt für unseren Newsletter anmelden

Alles Wissenswerte auf einen Klick:
Unser Newsletter bietet dir die Möglichkeit, dich ohne großen Aufwand über die aktuellen Themen bei codecentric zu informieren.