Skip to main content

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:

ModuleBuilds
StorageStorage accounts + containers, queues, tables, shares, locks
FunctionAppWindows · WebAppWindowsWindows Function apps and Web apps (+ plans)
CosmosDBCosmos DB accounts, databases, containers
SignalR · EventHubsReal-time and streaming
AppConfig · AppInsightsApp Configuration and Application Insights
DurableTaskScheduler · GrafanaDurable-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 one storage_account_nonsensitive object 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.
Reach for the module README for exhaustive inputs

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:

  1. 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 a README.md (keep it terraform-docs-generated so it can't drift).
  2. Pin its own dependencies — declare required_version and required_providers so the module builds identically wherever it's used.
  3. 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.
  4. Consume it from a main.<domain>.tf with source = "git::…/<Thing>?ref=v1.0.0" and a for_each over a new locals.<domain>.tf map — exactly the domain pattern.
Gotcha: upgrading a module is a deliberate act, never automatic

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​

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.