Adding a Component
You need to add a storage account for a new service, or a new function app, or a config key the
app will read. This is the most common change anyone makes to the stack, and thanks to the
map-driven domain pattern it's far smaller than it looks — most of the
"work" is already done by the for_each blocks. The repo has a golden pattern for it, and
this lesson walks it.
The one idea to hold on to
Adding a component is mostly adding a map entry. The domain's module block already iterates the map, so a new key becomes a real resource with no change to the resource block. Everything else — identity, RBAC, config, outputs — is a short checklist you apply only where the component actually needs it.
Walk the full workflow — click through each step, and notice how many are conditional:
Add it to the domain map
locals.<domain>.tfAdd one key to the domain’s config map. The key is the component name; the value describes what it needs. This is the whole "create the resource" step.
storage_configs = {
image_acquisition = { component = "image_acquisition", containers = [ … ] }
my_new_service = { component = "my_new_service", containers = [ … ] } # ← new
}Required every time, conditional when needed
Four steps always apply; the rest depend on what the component does:
Always:
- Add the map entry in
locals.<domain>.tf— the component and its shape. - Confirm the variable contract in
variables.tf— a typed, defaulted path for the config. - Let
for_eachmaterialize it inmain.<domain>.tf— usually no edit; just confirm the block consumes the fields you added. - Validate and plan —
fmt,init,validate, select the workspace,planagainst the tfvars. Read the diff before it can ever reach apply.
Only when it applies:
envs/<env>.tfvars— only if an environment needs a different value than the default.locals.identities.tf+main.roles.tf— only if the component must reach other services (aggregate its principal id, grant it least-privilege roles).locals.appconfig.tf— only if the running app reads new settings (publish endpoints as values, secrets as Key Vault references).outputs.tf— only if a pipeline or another stack consumes the value.
A worked example
Say a new service needs its own storage account with an exports container. The entire change
to create it is one map entry in locals.storage.tf:
locals {
storage_configs = {
# … existing components …
reporting = {
component = "reporting"
estimated_cost = "50"
containers = [ { name = "exports", container_access_type = "private" } ]
queues = []
tables = []
}
}
}
Run terraform plan -var-file=./envs/dev.tfvars and the for_each in main.storage.tf shows
one new storage account to add — no resource block was touched. If reporting's function app
needs to write to it, you'd then add its identity to locals.identities.tf and a
Storage Blob Data Contributor assignment scoped to that account in main.roles.tf, and
publish the blob endpoint to App Configuration. If it doesn't, you're already done.
The map key becomes part of the resource's state address (module.storage["reporting"]).
Renaming reporting to reports later doesn't rename the resource — Terraform reads it as
"destroy reporting, create reports," which for a stateful resource like storage means data
loss. Adding is safe; renaming a live key is a state operation, not a
text edit.
Wrapping up
Adding a component is adding a map entry and letting for_each do the rest, plus a conditional
checklist for identity, RBAC, config, and outputs — always finished with a workspace-correct
plan you actually read. Add new keys; never rename live ones. The workflow above is the
canonical order; keep the diff minimal and let the pattern carry the consistency.
The conventions that keep all those components coherent — names, identities, and roles — are next: Naming, Identity & RBAC.