Running a 79-Module Terraform Library Like a Product

Running a 79-Module Terraform Library Like a Product

TL;DR. A shared Terraform module library has two possible futures: product or landfill. Ours holds 79 modules consumed live from main by four landing zones — no pinning. The only reason that isn’t reckless is the factory around it: changed-module CI with native .tftest.hcl tests, a weekly full re-validation against fresh provider releases, a drift linter that patrols deliberately duplicated code, Renovate configured as a signal, not an order, security scans, a public mirror — and a weekly watcher for the failure mode nobody monitors: Azure retiring an API version your live resources depend on.

Every consumer of our module library resolves it the same way: git:: source, main branch, no ?ref=. A fix lands once and reaches every landing zone on its next plan. Which also means: a broken main reaches every landing zone on its next plan.

That trade wasn’t an accident — pinning across four consumers means version drift, backport requests and “which zone is on which release” spreadsheets. We chose live consumption and accepted its consequence: the library’s CI isn’t hygiene, it’s the contract that makes unpinned consumption survivable.

The factory

Seven pipelines, each guarding a different way a module library rots.

1. Changed-module CI, plus a weekly full sweep. A PR triggers a dynamic matrix over the modules it actually touched — format, init, validate, and the module’s native Terraform tests (.tftest.hcl), in parallel. The part that catches what PRs can’t: a weekly re-validation of every module in the library, because modules rot without anyone touching them — a new provider release changes a default, deprecates an argument, tightens a validation. The weekly sweep converts “silent rot discovered during someone’s incident” into “Monday morning warning.”

2. A drift linter for duplication we chose on purpose. Our orchestrator modules (the *Stack ones) deliberately inline copies of blocks from canonical modules instead of nesting module calls — flatter graphs, clearer plans. Copies drift; that’s their nature. Rather than pretending they won’t, a linter compares each inline copy against its canonical source and warns on divergence. Advisory during reconciliation, then strict. Accepted duplication plus a linter beats both hidden duplication and deep module nesting.

3. Releases with docs, even where the platform has no release object. A SemVer tag builds terraform-docs for every module and generates release notes from the git log. Azure DevOps has no GitHub-style Releases page — the artifacts attach to the run summary instead. The point isn’t ceremony: a consumer deciding whether to pin ?ref=v0.x needs to read what changed without diffing HCL.

4. Renovate, configured as a signal. Weekly, PR-only, never auto-merge. Provider floors stay permissive (~>), so a Renovate PR doesn’t mean “merge me” — it means “a newer provider exists; merge when a module actually needs it.” A companion freshness job warns about in-range releases Renovate can’t PR (modules don’t commit lock files — consumers do). Dependency automation that files tickets instead of taking decisions is the right amount of automation for a library other people build on.

5. Security scanning that doesn’t cry wolf. Checkov, Trivy and TFLint on PRs and weekly — soft-fail. On a module library, blocking on scanner findings mostly teaches people to stop reading scanner findings; the weekly report goes to someone whose job is to triage it.

6. A public mirror. Every push to main and every tag mirrors to a public repository. Partly open-source posture, mostly a forcing function: code written knowing it will be public is written cleaner.

7. The retirement watcher — the one nobody has. Renovate watches versions. Nothing in a normal setup watches Azure itself retiring things — an API version being disabled, a SKU going away — underneath resources that are already deployed. Azure Advisor publishes exactly this signal (“Service Upgrade and Retirement” recommendations, evaluated against your live resources). A weekly pipeline queries it across the landing zone subscriptions and surfaces every hit as a warning. Your provider can be fully up to date while the API version it deployed two years ago has a shutdown date — this is the only alarm that rings for that.

The migration tax, briefly

The factory started life as GitHub Actions and moved to Azure DevOps with the code. One portability lesson worth the paragraph: Azure Repos ignores the YAML pr: trigger entirely. PR validation only runs if a Build Validation branch policy points at the pipeline — with path filters doing the job of on: pull_request: paths:. It’s documented, it’s by design, and it will still cost you an afternoon the first time a pipeline silently doesn’t run.

The schedules are staggered on purpose — security scan, then Renovate, then the retirement watch, Monday morning in sequence. One coffee, one health report for the whole library.

The shape of it

If your module library has no CI, you still have CI — it’s called your consumers, and they didn’t volunteer. Every landing zone plan is implicitly integration-testing your main, in production, with an audience.

Start with the cheapest piece: the weekly full validate of every module against current providers. One scheduled pipeline, no refactoring, and it converts the scariest property of a shared library — silent rot — into a Monday morning list. The rest of the factory can grow from there, one failure mode at a time.

References

Related Posts

The AVM ALZ accelerator is solid. Here's what we add on top (and what we'd do differently)

The AVM ALZ accelerator is solid. Here's what we add on top (and what we'd do differently)

TL;DR. The official Azure/avm-ptn-alz/azurerm module covers the essentials of an Azure Landing Zone correctly. Three things aren't in the box and cost us time: explicit pinning of the alz/`a

read more
Building 60+ Terraform Modules for an Azure Landing Zone

Building 60+ Terraform Modules for an Azure Landing Zone

Why Build From Scratch? When I started building the Azure Landing Zone for a regulated European telecom operator, the obvious question was: _why not just use the Azure Verified Modules (AVM) direc

read more
The First Apply Is Local (and That's Fine)

The First Apply Is Local (and That's Fine)

TL;DR. The deployment chain that applies your IaC is itself infrastructure — and something has to create it first. Instead of hand-building the pipeline chain before the code could run, we appli

read more
The Second Landing Zone Is the Real Test of the First

The Second Landing Zone Is the Real Test of the First

TL;DR. We stamped out a second application landing zone from the first one. The diff between the two repos is six values: subscription, name acronym, address space, project, service connecti

read more
When terraform plan says 'no changes' and the feature is broken anyway

When terraform plan says 'no changes' and the feature is broken anyway

Everything was green. Workspace deployed, host pool up, users assigned, RBAC clean. Then a user opens Windows App and hits: "No devices or apps found." The feed never loads. Nothing in the Azure

read more
Why I Don't Use AVM Modules As-Is (And What I Do Instead)

Why I Don't Use AVM Modules As-Is (And What I Do Instead)

AVM Is Great — But Not For Everything Azure Verified Modules are the gold standard for Terraform on Azure. Microsoft maintains them, they follow

read more
GitOps Without PATs: Argo CD to Azure DevOps via Workload Identity

GitOps Without PATs: Argo CD to Azure DevOps via Workload Identity

TL;DR. Argo CD can authenticate to Azure DevOps repos with Entra Workload Identity instead of a PAT: a user-assigned managed identity, a federated credential bound to the `argocd-repo-server

read more
Azure Virtual Desktop: from POC to prod, the hardening checklist

Azure Virtual Desktop: from POC to prod, the hardening checklist

TL;DR. A fully private AVD POC that works is not a prod. Between the two: harden the FSLogix storage (NTFS ACLs, backup), automate secret rotation, wire up observability (AVD Insights + dedicate

read more
Deploying Azure Virtual Desktop End-to-End in a Landing Zone

Deploying Azure Virtual Desktop End-to-End in a Landing Zone

TL;DR. This is the deployment walkthrough for a fully private, Entra-only Azure Virtual Desktop environment inside a regulated Azure Landing Zone. Pooled Windows 11 multi-session on a **cust

read more
Azure Virtual Desktop in a regulated Landing Zone: the traps no doc tells you about

Azure Virtual Desktop in a regulated Landing Zone: the traps no doc tells you about

TL;DR. Deploying Azure Virtual Desktop fully private in a Landing Zone (Private Endpoints everywhere, Palo Alto NVA for egress, centralised DNS) works very well — once you know about a dozen

read more
GitOps Multi-Tenancy: the OPS/DEV Repo Split

GitOps Multi-Tenancy: the OPS/DEV Repo Split

TL;DR. Every application on our shared AKS cluster gets two repos: an OPS repo owned by the platform team (AppProject, namespace, ResourceQuota, LimitRange, the Application pointing at t

read more
Azure Landing Zone with Palo Alto NVA: Lessons Learned

Azure Landing Zone with Palo Alto NVA: Lessons Learned

Deploying Palo Alto VM-Series as a Network Virtual Appliance in an Azure Landing Zone sounds straightforward — until you actually do it. This article covers the hard-won lessons from building the fire

read more
SubnetWithNsg: How Azure Policy Deny Forced Me to Use azapi

SubnetWithNsg: How Azure Policy Deny Forced Me to Use azapi

The Problem Every production Azure Landing Zone deploys a Deny policy: "Subnets must have a Network Security Group." It is a security best practice — no subnet should exist without an NSG cont

read more