Skip to main content

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:

step 1 of 9always

Add it to the domain map

locals.<domain>.tf

Add 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:

  1. Add the map entry in locals.<domain>.tf — the component and its shape.
  2. Confirm the variable contract in variables.tf — a typed, defaulted path for the config.
  3. Let for_each materialize it in main.<domain>.tf — usually no edit; just confirm the block consumes the fields you added.
  4. Validate and plan — fmt, init, validate, select the workspace, plan against 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.

Gotcha: add a new key — never rename a live one

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.