From schema to AVM: the tfmodmake journey

From schema to AVM: the tfmodmake journey

What OpenAPI can generate, what still needs judgement, and why this tool keeps getting more useful.

From schema to AVM: the tfmodmake journey

How far can we take the Azure REST API specs towards automatically generating a high quality Terraform module?

That question has been bouncing around my head over the last few days.

Specifically: if you start from the OpenAPI schema alone, how much of an AVM-style module can you generate correctly, repeatably, and usefully?

TL;DR

  • The Azure REST API schema gets you surprisingly far: types, descriptions, validations, secret handling, and most of the Terraform structure you actually want.
  • This is not “AI generation” — it’s deterministic plumbing that turns Azure’s OpenAPI specs into something you can reliably iterate on.
  • Scaffold an AVM-style module in a single command — including child submodules and interfaces — directly from the REST specs.

That foundation is then ideal input for agent-based auditing and refinement.

If this sounds cool, then keep scrolling!

Where this started

Two posts from a couple of my hero engineers inspired this post.

Zijie’s approach uses AzureRM (and newres as an anchoring foundation) to produce AzAPI modules, with the goal of replicating AzureRM inputs and behaviour. I chose a clear break from AzureRM in favour of more deterministic inputs.

I love the approach from Zijie, and this isn’t in competition to that, rather to analyse how far the spec can go. The next stage could the same - leveraging Zijie’s work to increase capability not surfaced from the API specification - acknowledging the immense value in the AzureRM provider.

The origin

The original tfmodmake creates the variables and locals needed to build an AzAPI resource body from the REST specs.

  • It had a single input variable for properties (mimicking the AzAPI request body), which whilst accurate, was not very idiomatic Terraform.

  • It included an addsub component to create the wiring required to connect an existing submodule to its parent. (A common pattern in AVM used for sub-resources such as SQL databases)

Everything tfmodmake produces is deterministic: the same spec input produces the same Terraform every time. That’s important, because it gives you something stable to audit, extend, and reason about — whether by humans or agents.

The first improvement: escaping the properties blob

If you’ve worked with AzAPI, you’ll recognise this immediately.

Having a single properties variable is “correct”, but it makes for a much less pleasant authoring experience (filling out large variables is never fun!)

The first big improvement in the enhanced tfmodmake is to generate a more idiomatic interface: strongly typed variables with descriptions.

Generated Terraform variables with types, descriptions, and validation blocks derived from the Azure OpenAPI schema.

Secrets handling (without pretending the schema is perfect)

Secrets are decorated in the REST spec with x-ms-secret: true. Well, sometimes.

Sometimes the secret is in a nested object, sometimes it is hidden behind ambiguous descriptions.

For AzAPI modules, I want a default secure posture where we:

  • leverage the latest “write only” capabilities (using sensitive_body)
  • decorate secrets appropriately in variables and outputs (e.g. marking as ephemeral)

This is one of the places where schema + judgement wins, that said, you can do surprisingly well with just the schema:

Generated Terraform inputs showing secret fields marked as sensitive/ephemeral and written via AzAPI sensitive_body handling.

Validations

Where the Azure spec includes constraints (lengths, patterns, enums, ranges), you can turn that into Terraform condition validations.

A key aspect the tool ensures is that validations remain:

  • Null-safe (optional values shouldn’t fail validation)
  • Predictable (generated the same way every time)

Again, the coverage of the REST spec is not consistent (some resources have validation for resource names, others do not), but it is a great starting point.

Why allOf expansion matters

If you’ve worked with OpenAPI specs in the wild, or have had the fun of working with ARM, you’ve seen allOf.

Azure uses allOf heavily to compose schemas (common ARM shapes plus resource-specific properties).

If you don’t expand allOf, you end up with partial schemas:

  • missing properties,
  • incomplete validations,
  • and (sometimes) the wrong impression about what is writable.

So part of the “glue” work in tfmodmake is building an effective schema view that expands allOf into something generator-friendly.

Child discovery

The original included addsub, which creates a wrapper to wire a parent into an existing submodule.

E.g. given a keyvault module, and a pre-created secrets submodule, this command would create the following to wire the parent and child module together:

  • variables.secrets.tf (a map(object(...)) input)
  • main.secrets.tf (a for_each module block)

Whilst adding a submodule is useful - what about actually writing them for you?

Oh yes, we can.

A new gen submodule command does just that.

But wait..

Wouldn’t it be great if you could find all the relevant submodules, create them and wire them together?

Did Santa come this year?

This discovery process turned out to be trickier than it first looks.

Discovering child modules

Azure’s specs aren’t organised for “tell me the deployable children of this resource”. They’re organised for service teams publishing APIs.

In practice that means:

  • the parent and its children are often split across multiple spec files,
  • a service can have many versions (stable and preview),
  • some paths look like children but aren’t useful to generate (operations, config-only shapes, etc.),
  • and there are unusual edge cases (I’m looking at you, storage accounts - with singleton segments like default)

If you don’t handle these details, you either miss real child resources (worst case: you generate the wrong module set), or you drown in irrelevant noise.

The approach

We landed on an opinionated and deterministic solution for discovery.

Instead of asking people to hand-pick the “right” swagger files, we let them point at a service root directory and then apply consistent selection rules.

i.e. in simple terms - instead of pointing to a specific spec file like:

1
2
3
https://github.com/Azure/azure-rest-api-specs/blob/main/
specification/app/resource-manager/Microsoft.App/ContainerApps/
stable/2025-07-01/ManagedEnvironments.json

You point to the service root:

1
2
https://github.com/Azure/azure-rest-api-specs/tree/main/
specification/app/resource-manager/Microsoft.App/ContainerApps

The tool then applies consistent selection rules (latest stable API version by default, optional preview versions) to discover all relevant specs for that service.

End-to-end composition (gen submodule)

Then, we moved onto generating child modules, and wiring them into the parent in one go.

This is where the tool starts to feel like a module authoring assistant.

A typical flow looks like:

1
2
3
4
5
tfmodmake gen submodule \
  -parent "Microsoft.App/managedEnvironments" \
  -child "Microsoft.App/managedEnvironments/storages" \
  -module-name "storage" \
  -spec-root "https://github.com/Azure/azure-rest-api-specs/tree/main/specification/app/resource-manager/Microsoft.App/ContainerApps"

“But Santa, this is getting too hard”, so:

Orchestrating an AVM module

The gen avm command brings everything together. It’s an orchestration command that:

  1. Generates the base module - All the Terraform files for the parent resource (variables, locals, main, outputs)
  2. Discovers child resources - Scans the specs to find all deployable child resources
  3. Generates child submodules - Creates a complete submodule for each child in modules/<child>/
  4. Wires everything together - Generates the main.<child>.tf and variables.<child>.tf files to connect children to the parent
  5. Adds AVM interfaces - Generates main.interfaces.tf for common AVM patterns (private endpoints, diagnostics, customer-managed keys)

A typical invocation:

1
2
3
./tfmodmake gen avm \
  -spec-root "https://github.com/Azure/azure-rest-api-specs/tree/main/specification/app/resource-manager/Microsoft.App/ContainerApps" \
  -resource Microsoft.App/managedEnvironments

So, as promised: a single command, a few seconds, and you get this:

tfmodmake CLI output running the gen avm command to generate a base module, discover child resources, generate submodules, and add AVM interfaces.

We generate something like this:

Directory layout of the generated AVM module, including child submodules, parent-child wiring files, and the main.interfaces.tf AVM interfaces file.

For a Managed Environment, this generates:

  • Base module with full variable definitions, validations, locals, and outputs
  • 7 child submodules (certificates, dapr_components, dapr_subscriptions, http_route_configs, maintenance_configurations, managed_certificates, storages)
  • Parent-child wiring for all submodules
  • AVM interfaces for private endpoints, diagnostics, and customer-managed keys

All from a single command, in seconds.

What’s next

This is a foundation, not yet a finished product. The generated code gives you:

  • Type-safe inputs with schema-derived descriptions
  • Validations where the spec provides constraints
  • Secrets handling with ephemeral variables and sensitive outputs
  • Child module scaffolding ready to extend
  • AVM compliance patterns already wired

But it doesn’t give you:

  • Cross-field semantic validations (e.g., “if X is enabled, Y must be disabled”)
  • Domain knowledge about resource quirks or best practices
  • Opinionated defaults based on real-world usage
  • Examples and usage documentation (these can be sourced from AzureRM acceptance tests or AVM examples)

That’s where agent capabilities shine - auditing the generated foundation, filling gaps, and adding the judgment that only comes from experience.

The goal isn’t to replace human expertise or agent-based analysis. It’s to eliminate the repetitive plumbing so that both humans and agents can focus on what actually requires judgement.

If you’re working with Azure modules, try out my pull request. It might surprise you how far the specs can take you.

This post is licensed under CC BY 4.0 by the author.