The Wrapper Library
Last reviewed · content updated
IntermediateWhat you'll learn
~18 min- Choose an IaC language deliberately - the Bicep/Terraform doctrine, stated plainly
- Build thin wrappers over verified upstream modules with organization defaults baked in
- Keep wrapper interfaces stable so upstream swaps never touch consumers
First, the question every Azure learner asks
You’re about to watch an Azure shop build a Terraform wrapper library, and the fair question is “why not Bicep?” The current doctrine — Microsoft’s own, stated in their provider-selection guidance — is refreshingly non-tribal: Bicep for Azure-only estates (day-zero coverage of every ARM feature, no state file to manage), Terraform when the estate or the team’s future is multi-cloud or hybrid (one language, one workflow, the bigger module ecosystem) — with azurerm as the default provider supplemented by AzAPI for the bleeding edge. Meridian’s platform team chose Terraform: the estate already spans a sovereign-cloud region, the Zero Trust work touched non-Azure SaaS, and the routing framework (Lesson 1.3) reserves a provider axis. Neither choice is wrong; undocumented choice is wrong. Write yours down as an ADR (Lesson 3.5 shows the form) and move on.
The pattern: thin wrappers over verified upstream
The temptation with community/vendor modules is consuming them directly, forty callsites deep. The pattern that survives — running live in a real government estate with ~50 of these — is the thin wrapper:
modules/storage-account/ main.tf - calls ONE verified upstream module (Azure Verified Modules, in this estate) at an EXACT version pin, injecting the org's non-negotiables as defaults: public access off, TLS floor, diagnostics wired, the NIST-annotated settings from Lesson 3.2 variables.tf - the STABLE interface consumers see: a curated subset, org vocabulary, safe defaults outputs.tf - stable outputs, decoupled from upstream's namingThree properties do the work. Compliance is a default, not a memo — a team consuming modules/storage-account cannot forget the diagnostics setting, because forgetting isn’t expressible (the same can’t-opt-out instinct as 2.2’s stage scaffolding — one design idea, two artifact types). The interface is the contract — consumers bind to your variables.tf, so when upstream renames an input or you swap the verified module for a raw-resource fallback (the estate documents that fallback procedure explicitly — upstream modules get abandoned, licenses change, worlds move), consumers change zero lines. Pins are exact — the wrapper’s version = "0.14.3", never ~> — with the whole pin story (central provider constraints, the human-readable version matrix, upgrade cadence) getting Lesson 3.3 to itself.
The honest counter-example, on schedule
This training promised to teach from a real estate’s warts as well as its wins, and here’s the first: the same exemplary wrapper library authenticates its pipelines with a client secret exported into ARM_CLIENT_SECRET — no workload identity federation anywhere — and, better still, its README claims certificate auth while the code path says secret. Two lessons in one wart: the secretless migration you should run instead is taught in the Zero Trust training and Cloud Modernization’s machine-identity lesson (pointer, not re-teach); and the doc-vs-code mismatch is Lesson 1.2’s audit habit striking again — even good estates’ docs lie; grep the code.
Build our first wrapper: modules/storage-account over the verified upstreamstorage module. (1) exact-pin the upstream; (2) inject as non-overridabledefaults: public network access disabled, min TLS 1.2, blob soft-delete,diagnostics to the central workspace (control IDs annotated in comments);(3) expose a curated variables.tf - name, resource group, SKU tier(constrained enum), and an explicit exceptions object for the rareoverride, each use requiring a reason string; (4) outputs.tf with stablenames; (5) a consumer example, and the FALLBACK note: what re-implementingthis wrapper on raw azurerm resources would take if upstream vanished.Then the doc-vs-code audit from 1.2, pointed at ourselves: does anythingour module README claims differ from what the code does?The exceptions-object detail rewards attention: overrides exist (real estates need them) but each carries a required reason string — reviewable in the plan output, greppable across the estate. Exceptions with attached reasons are a pattern you’ve now met in three trainings; in IaC they cost one variable definition.
Fifty wrappers is where the reference estate ended, not where it started. Wrap a service when the SECOND team needs it — the first consumption teaches you the right interface; premature wrapping guesses at it. A wrapper library is a product (Module 2’s lesson exactly), and products grow by demand.
A teammate reviews the wrapper plan and objects: 'This is a pointless layer — the Azure Verified Module already exists, is maintained by Microsoft, and takes the same inputs. Consuming it directly saves fifty files of indirection.' What's the strongest answer?
Key takeaway
Choose the language by doctrine and document it; then wrap verified upstream modules thinly — exact pins, org controls as inexpressible-to-forget defaults, curated stable interfaces, reasoned exceptions, and a written fallback path. And keep auditing docs against code, including your own. Next: where those org controls actually come from — the inheritance tree.