The Domain Pattern
There are over a dozen domains in the stack — storage, functions, backend APIs, databases, Event Hubs, and so on — and they're all built the same way. Learn the pattern once and every domain reads the same. This lesson walks it with the storage domain, but the shape is universal.
The one idea to hold on to
Configuration lives as data in a locals map, and a single for_each module block turns
every entry in that map into real resources. Adding a component is adding a map key; the
resource block never changes. This is "map-driven" infrastructure, and it's the single most
important convention in the repo.
Concretely, storage is two files. First, the configuration — a map where each key is a
component and each value describes what it needs (locals.storage.tf):
locals {
storage_configs = {
image_acquisition = {
component = "image_acquisition"
estimated_cost = "464"
containers = [ { name = "images", container_access_type = "blob", /* … */ } ]
queues = [ /* … */ ]
tables = [ /* … */ ]
}
# add another key here → another storage account appears
}
}
Then, the materialization — one module block that iterates the map (main.storage.tf):
module "storage" {
source = "git::https://…/Terraform.Module.Azure.Storage?ref=v1.0.1"
for_each = local.storage_configs
storage_account_name = substr(format("%s%s", local.naming_convention_storage_account, var.resource_identifer), 0, 24)
resource_group_name = azurerm_resource_group.alertca.name
tags = merge(local.tags, { estimated-cost = each.value.estimated_cost }, { Component = each.value.component })
storage_container = each.value.containers
storage_queue = each.value.queues
storage_table = each.value.tables
}
each.value is the map entry; each.key is the component name. One block builds one account
or a hundred, and it never needs editing to add the hundred-and-first — you edit the map.
A component is more than its resource
Building the storage account is only the first of five steps. A real service also needs an identity to act as, permission to reach its dependencies, configuration the app can read, and sometimes an output for a downstream consumer. Those are the other domain files, and they wire together like this:
Reading that flow as prose: the module block produces a managed identity (a principal id)
and connection details as outputs; locals.identities.tf collects those principal ids into
a central place; main.roles.tf grants them least-privilege roles on the resources they
need; locals.appconfig.tf publishes the endpoints/keys the running app reads at startup;
and outputs.tf exports anything a pipeline or another stack consumes. Each domain plugs
into the same five sockets.
This is why "how components work together" has a crisp answer here: they don't call each other in the Terraform — they're joined through identities, roles, and App Configuration. The storage account never references the function app; instead the function app's identity is granted a role on the storage account, and the storage endpoint is written to an App Config key the function reads. Terraform wires the permissions and configuration; the runtime does the rest.
variables.tf is the contract, locals is the compositionIt's easy to put a value in the wrong place. Rule of thumb: variables.tf holds the typed,
defaulted, validated input contract (things an environment might override); locals.*
holds derived and composed values built from those inputs (the maps, the names, the tags).
A new tunable that an environment sets goes in variables.tf (and maybe envs/*.tfvars); a
new component assembled from existing inputs goes in a locals map.
Wrapping up
Every domain follows one pattern: configuration as data in a locals map, one for_each
module block that materializes it, and then identity → RBAC → App Config wiring that joins the
component to the rest of the platform. Components don't reference each other directly; they're
connected through granted roles and published configuration. Adding one is adding a map entry —
which is exactly what the Adding a Component walkthrough will make
concrete.
First, the things those module blocks point at. Next: Reusable Modules.