Reusable Modules
The root stack composes; it rarely builds. Open a main.<domain>.tf and you'll mostly see
module blocks, not raw azurerm_* resources. Those modules are separate, versioned
repositories — one per kind of Azure resource — and they're the reusable building blocks the
whole platform (and other teams) share.
The one idea to hold on to
A module is a self-contained, version-pinned repository that builds one Azure resource well: a typed variable contract goes in, structured outputs come out. The root stack consumes it by git source, locked to a tag:
module "storage" {
source = "git::https://…/Terraform.Module.Azure.Storage?ref=v1.0.1"
# inputs (the module's variable contract) …
}
That ?ref=v1.0.1 is not optional decoration — it pins the module to an exact release, so a
plan is reproducible and an upstream change can't silently alter your infrastructure. Pinning
is the single most important module practice; the gotchas lesson
returns to why.
The catalog
Each module is its own Terraform.Module.Azure.* repository:
| Module | Builds |
|---|---|
Storage | Storage accounts + containers, queues, tables, shares, locks |
FunctionAppWindows · WebAppWindows | Windows Function apps and Web apps (+ plans) |
CosmosDB | Cosmos DB accounts, databases, containers |
SignalR · EventHubs | Real-time and streaming |
AppConfig · AppInsights | App Configuration and Application Insights |
DurableTaskScheduler · Grafana | Durable-task scheduling and dashboards |
The contract every module honors
Modules aren't ad hoc — they share a shape, which is what makes them predictable to consume:
variables.tf— the input contract: typed, defaulted, and validated. The Storage module alone exposes dozens (account_kind,account_replication_type,network_rules,blob_properties,identity,management_locks, …), each with a sensible default so callers set only what they mean to change.main.tf(+main.<sub>.tf) — the resources, split by sub-concern (main.containers.tf,main.queues.tf,main.tables.tf,main.locks.tf).outputs.tf— structured outputs. Storage exposes onestorage_account_nonsensitiveobject carrying every id, host, and endpoint the caller might wire onward (blob/queue/table endpoints, primary/secondary hosts, the account id). This is what the root stack feeds into identities and App Config.README.md— a thorough,terraform-docs-generated reference: overview, data-protection / HA / DR considerations, required permissions, a usage example, and full input/output tables.
This section teaches the pattern; it deliberately does not reproduce each module's full input
table (dozens of variables that would go stale here). When you need the exact inputs for a
module, read that module repo's README.md — it's generated from the code, so it's always
current.
Adding a new module
You add a module when a new kind of Azure resource enters the platform and deserves a reusable, well-tested wrapper (as opposed to a one-off resource in the root stack). The steps:
- Create the repo
Terraform.Module.Azure.<Thing>, following the contract above:variables.tf(typed, defaulted, validated inputs),main.*.tf(resources, split by sub-concern),outputs.tf(a structured output object), and aREADME.md(keep itterraform-docs-generated so it can't drift). - Pin its own dependencies — declare
required_versionandrequired_providersso the module builds identically wherever it's used. - Tag a release (
v1.0.0) so consumers can pin to it. Modules are versioned by git tags, semver-style: breaking input/output changes bump the major. - Consume it from a
main.<domain>.tfwithsource = "git::…/<Thing>?ref=v1.0.0"and afor_eachover a newlocals.<domain>.tfmap — exactly the domain pattern.
Because sources are pinned, bumping ?ref=v1.0.1 → v1.1.0 is a code change that must be
reviewed and planned like any other. Read the module's changelog for input/output changes,
bump the ref, and inspect the plan before applying — an output that was renamed or a default
that changed can ripple into identities, roles, and config. Never widen a pin to a branch or
latest to "stay current"; that trades reproducibility for surprise.
Further reading
- Terraform module structure & best practices
- Module sources (git refs and pinning)
- Publishing and versioning modules
Wrapping up
Modules are the platform's version-pinned building blocks: one repo per Azure resource type,
each with a typed-input / structured-output contract and a generated README, consumed by the
root stack through a git source locked to a tag. Adding one means writing that contract, tagging
a release, and wiring it in via a domain map; upgrading one is always a deliberate, planned
bump. For the exact inputs of any module, read its own README.
Next, how the composed stack becomes five running environments: Environments & Deployment.