Recently I was introduced to formae, an open source IaC tool developed by Platform Engineering Labs. formae aims to shift how we manage Infrastructure as Code, implementing different approaches and paradigms then Terraform or Pulumi.
At its core is an (non-ai) agent that manages and continuously monitors your environment. This enables automatic resource discovery, meaning formae can detect infrastructure even if it was created through ClickOps or by other IaC tools. Because the agent maintains a constant view of the actual state, the common issue of state drift is effectively prevented. Unlike traditional tools where you must manually import resources into a state file first, formae can directly generate the corresponding infrastructure code so you can use it immediately.
When it comes to deployment, it supports both classic GitOps workflows and an experimental No-Git GitOps approach where the tool itself acts as the system of record. This allows teams to choose between strict Git-driven control or a more flexible model where Git becomes optional and infrastructure can be managed using temporary configuration files.
For more details, their documentation covers a lot of topics from fundamentals to CI/CD integration, security and networking.
Terraform vs. formae
If you work as a Cloud, DevOps, or Platform Engineer, you have almost certainly had your fair share of experience with Terraform. As the de facto standard, its community is massive, and there is a provider for almost everything. Nevertheless, I bet you have had your painful moments with Terraform:
- import already existing resources, not managed by Terraform yet
- drifts between Terraform Config and Infrastructure
- state locks
- remote state handling
The State File vs. Live Reality: In Terraform, the .tfstate file is considered the absolute source of truth. If someone modifies a resource via ClickOps, a script, or another tool, Terraform remains entirely unaware of out-of-band modifications until a manual state synchronization is forced. When code and actual infrastructure fall out of sync, fixing the drift becomes a complex task. formae takes a different stance: your Pkl code stays authoritative for what you actively manage, while the agent's live view of the cloud tracks everything else, continuously capturing out-of-band changes and versioning them into an internal state. In practice, this means you can bring formae into an existing Azure setup without first tearing down what Terraform, scripts, or ClickOps left behind.
Global Stack Updates vs. Granular Patching: A classic Terraform workflow applies infrastructure changes holistically across an entire stack. If a single resource transition encounters an error or a provider misbehaves, the entire deployment pipeline can stall, potentially impacting other resources. formae alters this deployment model by executing stack changes asynchronously through isolated finite state machines. It introduces a "Patch Mode" allowing operators to apply precise, granular property updates or hotfixes directly to specific resources via code, significantly minimizing the operational blast radius.
Static DSL vs. Statically Typed Configuration: Terraform relies on HashiCorp Configuration Language (HCL). While mature, HCL is fundamentally a domain-specific language that can struggle with complex logic, often requiring workarounds or verbose template functions to handle conditionals, loops and deeply nested data structures. formae instead uses Apple's open-source Pkl language. Pkl bridges the gap by providing a statically typed language designed specifically for configuration. It brings native capabilities like type safety, inheritance, and built-in validation rules directly to your infrastructure files, catching errors during evaluation before any code ever touches your cloud provider. Platform Engineering Labs offers a 16-Step hands-on tour here.
Intro
This blog post is part of the formae by Example series, with all companion code available in a dedicated GitHub repository. Each part is organized into a self-contained folder designed to be read top-to-bottom, evaluated, and applied directly. While future parts are planned to cover remote agents, CI/CD integration, and driving formae through an MCP server, Part 1 focuses on the basics and walks through a practical Terraform-to-formae migration of an Azure stack.
formae from Scratch — Azure SQL behind a Private Endpoint
Prerequisites
Install formae:
1/bin/bash -c "$(curl -fsSL https://hub.platform.engineering/get/formae.sh)"
Add the binary to your PATH:
1echo 'export PATH=/opt/pel/bin:$PATH' >> ~/.zshrc
2source ~/.zshrc
For bash or other shells, see local install.
Verify:
1formae --version
Azure CLI logged in and the right subscription selected:
1export AZURE_SUBSCRIPTION_ID=<your-sub-id>
2az login
3az account set --subscription $AZURE_SUBSCRIPTION_ID
A running formae agent with the plugin you import:
1formae plugin install azure@0.1.6
2formae agent start
Structure
Instead of walking through all eleven resources line-by-line, we'll focus on the structural highlights. (If you are new to the syntax, refer to the official fundamentals in the formae documentation). Looking at our main.pkl, the file is organized into three primary blocks: description, properties and forma.
First is the description block, which serves as your configuration's metadata. It’s essentially self-documenting code.
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}
Each property defined in the properties block automatically becomes a command-line flag, allowing you to customize your deployment without modifying the code itself.
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}
You can inspect all available properties dynamically by running:
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[...]
The forma block is where the actual infrastructure lives:
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, declaring an object with the local modifier makes it a private, non-renderable property. It is evaluated in memory, but it will not be written to the output file that formae hands over to your cloud provider. To actually emit the resource into formae's deployment manifest, you must reference it outside of the local scope (the trailing rg line shown above).
Once defined, you can seamlessly cross-reference it in other resources:
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}
Notice how properties are referenced in two distinct ways here: rg.location and rg.res.name.
rg.location: If you are passing down configuration values that are entirely static (like locations or tags), e.g. coming from a command-line flag, reading them directly from the local Pkl object reference is perfect. It allows formae to evaluate the value instantly.
rg.res.name: Whenever you are linking resources together for deployment dependencies, always chain them via .res (e.g., rg.res.name, snet.res.id). This explicitly signals to the formae engine that it must resolve the resource's state from the agent/cloud provider before building the final execution graph.
Eval and Apply
Now that we have looked into the main.pkl, let's deploy some resources in our Azure Subscription: An Azure SQL Server locked down to a private network, with a User-Assigned Managed Identity as its only admin.
Checkout this repository from github: formae-example. Navigate to part-1-intro/01-from-scratch and evaluate the forma:
1formae eval main.pkl
This will show you the resources formae wants to create in a json format.
Now apply it:
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
After a few seconds or minutes all resources are created:
Check which resources are managed by formae:
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 and Patch Mode
There are two apply modes: reconcile and patch. From the formae documentation:
Reconcile mode (--mode reconcile, default): The desired state is the complete truth. Elements not in your forma file are removed.
Patch mode (--mode patch): Can create and update. It never deletes anything. If a resource exists in your cloud but is completely missing from your main.pkl, Patch mode ignores it.
| Behavior | --mode reconcile | --mode patch |
|---|---|---|
| Philosophy | The file is the absolute truth. | Add without removing. |
| Missing Resource | Destroyed in the cloud. | Left alone. |
| Out-of-band Tags | Stripped away. | Preserved. |
| Drift Safety | Fails if cloud drifted since last apply. There is a --force flag that allows to undo all out-of-band changes without failing | Never fails on drift, append-only merge |
You applied the formae Config before with the reconcile mode, now let’s test the patch mode:
Add another database:
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}
Then use mode path this time:
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
The Output shows only the new database is created. After removing it from main.pkl and re-run --mode patch, nothing happens:
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.
Switching back to --mode reconcile: It refuses, because patch created drift from reconcile's checkpoint, and reconcile won't silently delete something it didn't know about.
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>
You either re-apply with --force (reconcile wins, logdb is deleted) or run formae extract to pull logdb into a forma file. We will have a look at this later.
Before going to the next chapter (02-terraform-original) you can tear down the resources:
1formae destroy --query 'stack:stack-formae-demo-dev' --watch
The Terraform equivalent — Azure SQL behind a Private Endpoint
Before we look at how to bring existing resources under formae's control, we actually need some infrastructure to work with. Rather than clicking around the Azure Portal to build this manually, we’re going to provision our initial environment using Terraform.
This approaches a common real-world scenario. Since Terraform is widely used across the industry, adopting a new tool like formae often begins with migrating or connecting to infrastructure that was originally provisioned by a classic Terraform setup. We will not go into detail about the Terraform configuration here.
Navigate to part-1-intro/02-terraform-original and apply the Terraform config:
This contains the same resource stack we created earlier with formae.
Set your subscription_id first:
1export TF_VAR_subscription_id=<your-sub-id>
Initialize:
1terraform init
Apply:
1terraform apply -auto-approve
After some seconds or minutes all resources are created:
Do not tear down these resources yet, we need them in the next chapter.
The migration — put formae in front of the running stack
Now that our infrastructure is up and running via Terraform, we face a classic day-two operations challenge: How do we hand over control to formae without tearing anything down and causing downtime?
In a traditional workflow, adopting a new IaC tool usually requires a tedious import process, mapping resource IDs one by one into a new state file. formae approaches this differently by shifting the focus from static state files to active management.
Let's look at how formae discovers these unmanaged resources and gracefully takes over control over them.
1. Resource Discovery
formae actively finds what's in your subscription. The formae agent scans your cloud targets on a fixed interval (5 minutes by default). Because we deployed our baseline in the previous chapter, those resources are already sitting in Azure, categorized by the agent as unmanaged.
You can query this unmanaged inventory directly from your terminal using wildcards:
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
Without the label it would show all resources in that Azure Subscription that are not managed by formae.
When running our initial wildcard inventory query (label:*tf-demo-dev*), you will notice that formae returns 11 resources. However, our architecture actually consists of 13 moving parts. Two crucial resources will be missing from that first list: the Private DNS Zone and the Role Assignment.
Why didn't the wildcard catch them? It comes down to how Azure names resources:
- The Private DNS Zone: To work correctly with Azure SQL, the zone must be globally named exactly
privatelink.database.windows.net. It cannot include your custom workload prefix or instance suffix. - The Role Assignment: Azure generates Role Assignments using a completely random, unique GUID (e.g.,
26ed4d89-2633-f4a6-37b6-...) as the resource name.
Because neither of these resource names contains the tf-demo-dev string, formae's wildcard filter skips them. To find and capture these outliers so we can migrate them alongside the main stack, we simply query for them by their explicit Azure resource types:
1formae inventory resources --query="managed:false type:AZURE::Network::PrivateDnsZone"
1formae inventory resources --query="managed:false type:AZURE::Authorization::RoleAssignment"
If your Azure Subscription has a lot of active RBAC assignments, the second command might return a massive list. You can easily find the right one by scanning the output's NativeID column for the row that explicitly mentions your resource group (rg-tf-demo-dev-001). Copy its unique GUID label, you need it in the extract command later (see Section 2. b).
2. The Migration Workflow:
a. Extract and Adopt
Instead of writing Pkl code from scratch to match what Terraform built, you let formae write it for you.
1formae extract --query="managed:false label:*tf-demo-dev*" ./discovered.pkl
This generates a fresh discovered.pkl file mapping out your real-world resources. To transition these resources from unmanaged to a fully managed state, you only need to modify one thing inside the extracted file: the 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}
⚠️ The master database gotcha
Azure auto-provisions a master database under every logical SQL Server. formae extract picks it up and emits a block for it. Adopting that block is harmless but the moment you reconcile a version of the file that no longer contains it, formae tries to delete it and Azure refuses with CannotUseReservedDatabaseName:
1Cannot use reserved database name 'master' in this operation.
The server cannot be deleted and the apply fails.
This isn't a formae limitation. Run
1az sql db delete -n master -s sql-tf-demo-dev-001 -g rg-tf-demo-dev-001
yourself and Azure returns the same error, master is reserved and can't be dropped at all. The simplest workaround is to strip the master block from your extracted Pkl before the first apply, and accept that formae inventory resources --query="managed:false" will always list one dangling master per SQL server.
After removing the master database object, you can apply this to safely transition ownership of those 10 resources over to formae.
1formae apply --mode reconcile --yes --watch --status-output-layout detailed discovered.pkl
Now verify resources are managed by formae:
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. Adopt the two outliers
We saw in the previous section that we need to extract the Private DNS Zone and Role Assignment separately.
1formae extract --query="managed:false type:AZURE::Network::PrivateDnsZone" ./dns.pkl
1formae extract --query="managed:false label:<ROLE_GUID_COPIED_FROM_BEFORE>" ./ra.pkl
Copy the PrivateDnsZone and RoleAssignment blocks from those files into discovered.pkl (and add any missing import lines at the top). Re-apply with --watch again:
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
Now the inventory command shows 12 resources:
1formae inventory resources --query="stack:stack-tf-migration-dev"
That's it, now the deployed resources from Terraform are under formae’s management.
3. Detecting and Handling Drift
What happens if reality diverges from your code? To show how protective formae is over your infrastructure state, the companion repository includes two drift scenarios: mutating a tag directly via the Azure Portal, or altering the values back in your original Terraform directory.
This design concept is what the formae ecosystem calls "co-existence". It doesn't blindly ignore changes by other tools like Terraform; it actively observes the same cloud environment and surfaces divergences so you can resolve them on your own terms.
To see formae's drift detection in action, let’s simulate a classic real-world issue: out-of-band changes. Imagine a colleague logs into the Azure Portal (or runs an az CLI command) and manually modifies a resource group tag without updating the infrastructure code:
1az group update -n rg-tf-demo-dev-001 --tags drift_test=v1 owner=alice
Once this happens, the live state of your cloud has diverged from your declaration. Because the formae agent continuously scans your environment, it will pick up this delta on its next sync (default is every 5 minutes).
If you attempt to run a standard deployment now:
1formae apply --mode reconcile --yes discovered.pkl
formae will actively guard your stack. Instead of blindly running or hammering state, the engine locks down and rejects the command with a clear warning:
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>
When drift occurs in reconcile mode, formae surfaces the exact resource that diverged and hands you two clear paths to resolve the situation.
Path A: Reject the Drift (Pkl Wins)
If the manual change was a mistake or an unauthorized modification, you can declare your Pkl file as the absolute source of truth. By appending the --force flag, you tell the formae agent to deliberately overwrite the drift and bring Azure back to your code's intent:
1formae apply --mode reconcile --yes --force --watch --status-output-layout detailed discovered.pkl
The Result: The agent executes the override, and the out-of-band owner=alice tag is removed from the Azure resource group.
Path B: Accept the Drift (Cloud Wins)
If the manual change was intentional (for instance, an emergency hotfix), you can absorb it into your codebase. formae actually prints the exact formae extract command you need directly inside the error message.
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
You simply copy that updated tags block, paste it back into your main discovered.pkl file, and re-run your apply. The engine reconciles happily because your code and the cloud are back in perfect alignment.
Tear down:
1formae destroy --query 'stack:stack-tf-migration-dev' --watch
Conclusion and outlook
Part 1 stayed deliberately narrow: one Azure stack, one localhost agent, one machine. Even at that scope, the pattern is different enough from a Terraform workflow to be worth showing.
What formae actually changed here
- Adoption is a query, not an import script.
formae extractproduced a working Pkl file for ten resources in seconds. The equivalent in Terraform is oneterraform importper resource plus hand-writing the HCL to match and getting the resource addresses right on the first try. - Drift Visibility in action. The agent notices the manual update before you do, and
applyrefuses to run over it. You get an explicit two-way choice: Pkl wins (--force) or cloud wins (extract + merge) instead of a silent "next apply will just overwrite it" behavior. - Patch mode is a real tool, not a workaround. Adding one database without touching the stack around it and having a monitoring team's tags survive the next reconcile is genuinely hard in a stack-wide apply model.
What Part 1 does not tell you
- The agent ran on your laptop. Nothing here shows what happens when the agent lives on Azure Container Instances, when three engineers share a stack, or when a CI pipeline calls
applyon merge. - The demo has 13 resources. Extract, apply, and drift-detection latency at 500 or 5,000 resources is a different question.
- Pkl's real payoff: type safety, module reuse, catching config errors before they touch Azure.
- Ecosystem gap. Terraform has a provider for everything; formae's plugin coverage is much smaller today. Check Azure supported resources.
If you're evaluating formae, the honest test isn't the from-scratch demo, it's the drift scenario in section 3. Break something out-of-band and see whether your team can live with the workflow when reality diverges from code. If yes, the rest follows.
Adopting formae does not require a complete, big bang rewrite of your existing infrastructure. Thanks to its co-existence model, you can keep your stable Terraform pipelines running exactly as they are and introduce formae selectively for some parts of you infrastructure. This incremental approach keeps adoption risk to a minimum. The tradeoff is that you then need to run two control planes.
The boundaries of ownership between what Terraform and what formae manages must be explicit, or you will quickly run into conflicting changes and recreate the exact drift problems you tried to solve.
If your existing Terraform pipelines are stable and you aren't feeling the pain of state friction or drift today, formae probably isn't a priority for you yet. It delivers its real value when you are managing complex migrations or building environments where multiple teams and automation tools need to safely co-exist.
What's next in the series
| Part | Focus | Problem it addresses |
|---|---|---|
| Part 2 | Remote agent on ACI, CI/CD integration, Observability | Turns the laptop demo into a scalable team workflow. formae’s active agent model bypasses the traditional complexities of remote backend configurations and state synchronization altogether |
| Part 3 | Driving formae through an MCP server | What does "let an LLM own a stack" look like when the tool already knows how to say reality drifted, human decides? |
Follow the repo at waldemarschmalz/formae-example. Each part ships as its own self-contained folder.
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.
Blog author
Waldemar Schmalz
Senior IT Consultant
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.