By the end of this you have cost views and budgets scoped by tag with alerts, tag inheritance turned on so those views are actually populated, a subscription-vending checklist to run the day a second subscription is justified, an add-a-spoke runbook that peers a new workload network into the hub and inherits everything the tree already enforces, a short set of health and drift checks that make up the monthly loop, and an end-to-end test that proves the loop actually fires. This is the operating discipline made concrete, the part where the foundation stops being built and starts being run. Northfork Supply Co. is the running example, with its single Production subscription under the tree.
Before Step 1: the foundation from the earlier articles is built, the six-tag policy is enforcing, and you have Cost Management access on the subscription (Cost Management Contributor, or Billing Reader for read-only views). Set the identifiers the commands use: the Production subscription is PROD_SUB_ID, and the action group you created for alerts in the logging build is ACTION_GROUP_ID. The management-group scope is /providers/Microsoft.Management/managementGroups/mg-northfork.
Step 1: Turn on tag inheritance, and see cost by tag
The tags you enforced in the policy article do not reach the billing data on their own, and this is the step everyone skips and then wonders why cost-by-tag is empty. In Microsoft Cost Management, turn on tag inheritance so that subscription and resource-group tags are stamped onto the child usage records, which is what lets you slice spend by CostCenter and Owner. Then build a cost view grouped by the CostCenter tag. A few resource types never emit tags into usage, so the view is strong rather than total, but it is the difference between a cost report you can act on and a single undifferentiated number.
| Setting | Value | Why |
|---|---|---|
| Tag inheritance | On (Cost Management, Manage tag inheritance) | Applies subscription and resource-group tags to usage records; without it, cost-by-tag is blank |
| Cost analysis grouping | Group by tag: CostCenter (and Owner) | Turns the enforced tags into per-team spend |
| Expectation | Some resource types show no tag in usage | Purchases and a few types never emit tags; the view is strong, not perfect |
Tag inheritance is a Cost Management setting rather than a resource, so it is turned on in the portal under Cost Management, in the tag-inheritance settings, and there is no clean command-line equivalent for the toggle itself. The cost view, once inheritance is on, is queryable.
# Month-to-date cost grouped by the CostCenter tag (after tag inheritance is on)
az costmanagement query --type Usage \
--scope "/subscriptions/PROD_SUB_ID" \
--timeframe MonthToDate \
--dataset-aggregation '{"totalCost":{"name":"Cost","function":"Sum"}}' \
--dataset-grouping name="CostCenter" type="TagKey" -o table
Expected result: spend broken out per CostCenter value. Failure mode to avoid: querying cost by tag before enabling tag inheritance, getting an empty or unattributed result, and concluding the tags were pointless when the real issue is one unset toggle.
Step 2: Budgets by tag, with an alert before the surprise
A cost view tells you what happened; a budget tells you before it gets worse. Create a budget on the subscription, filtered to a CostCenter value, with alert thresholds at eighty and one hundred percent wired to the action group you already built for operational alerts. Budgets attach action groups cleanly at subscription and resource-group scope, which is exactly the scope you want here, so this is where the alerting lives rather than at the management group.
| Setting | Value | Why |
|---|---|---|
| Scope | The Production subscription | Budgets attach action groups cleanly at subscription and resource-group scope |
| Filter | Tag: CostCenter equals the team value | Turns the budget into a per-team ceiling, not a whole-estate number |
| Thresholds | 80% and 100%, actual and forecast | The eighty-percent alert is the one that gives you time to act |
| Action | The ACTION_GROUP_ID from the logging build | Reuse the operational alert channel rather than inventing a new one |
# Create a monthly budget on the subscription with 80% and 100% alerts
az consumption budget create \
--budget-name "budget-costcenter-it" \
--amount 2000 --category Cost --time-grain Monthly \
--start-date "2026-08-01" --end-date "2027-08-01" \
--scope "/subscriptions/PROD_SUB_ID"
# Then, in Cost Management > Budgets, add the CostCenter tag filter and attach
# ACTION_GROUP_ID to the 80% and 100% alert conditions. The tag filter and action-group
# wiring are cleanest in the portal; the CLI creates the budget shell.
Expected result: a per-team budget that emails the operations channel at eighty percent, with time to do something about it. Failure mode: setting only a hundred-percent alert, which tells you after the money is already spent instead of while you can still change course.
Step 3: The subscription-vending checklist
When the day comes that a second subscription is genuinely justified, the first developer who must not touch production, a compliance boundary, a real isolation need, you do not improvise it. You run the same checklist every time, so that a new subscription arrives already governed rather than as an ungoverned island someone has to remember to bring into the fold. The single move that does most of the work is placing the subscription into the right management group, because from that moment it inherits the whole tree: the policies, the diagnostic settings, the RBAC scoping, all of it.
- Create the subscription through your agreement (Partner Center for CSP, or the enterprise agreement), named to the convention.
- Place it into the correct management group, Non-Production for an isolation or dev need, so it inherits the tree immediately.
- Expect and triage the compliance flood as the inherited policies evaluate it; this is normal, not a failure.
- Assign the RBAC groups at the new subscription scope, Contributor to the platform admins, Reader and Security Reader as inherited from the tree.
- Confirm the diagnostic-settings policy reaches it and run a remediation so its logs flow to the central workspace.
- Confirm it carries the mandatory tags, and place a delete lock on anything in it that becomes critical.
# The move that inherits everything: place the new subscription into the right MG
az account management-group subscription add \
--name "mg-nonproduction" \
--subscription "NEW_SUB_ID"
Expected result: a new subscription that is governed the instant it enters the tree. At the scale where you are doing this often, the subscription-vending module in the Azure Verified Modules landing-zone accelerator runs this whole checklist as code; the checklist is what you run by hand until that scale arrives.
Step 4: The add-a-spoke runbook
Adding a workload network is the most common growth move, and it is cheap precisely because the hub and the addressing were designed for it. Create the spoke in the address space you reserved, peer it to the hub in both directions with gateway transit so it can reach on-premises through the hub’s gateway, link it to name resolution, and let it inherit the diagnostic-settings and tag policies without touching them. The spoke shows up already logging, already tagged, already resolving, because inheritance was the point.
# Create the spoke in the reserved address space (next block after prod and non-prod spokes)
az network vnet create \
--resource-group "rg-network-prod-wus2-001" \
--name "vnet-spoke-app-prod-wus2-001" \
--address-prefixes "10.40.3.0/24" \
--subnet-name "snet-app" --subnet-prefixes "10.40.3.0/26"
# Peer hub to spoke, allowing the spoke to use the hub's gateway
az network vnet peering create \
--resource-group "rg-network-prod-wus2-001" \
--name "hub-to-spoke-app" --vnet-name "vnet-hub-prod-wus2-001" \
--remote-vnet "vnet-spoke-app-prod-wus2-001" \
--allow-vnet-access --allow-forwarded-traffic --allow-gateway-transit
# Peer spoke to hub, using the hub's remote gateway for on-premises reach
az network vnet peering create \
--resource-group "rg-network-prod-wus2-001" \
--name "spoke-app-to-hub" --vnet-name "vnet-spoke-app-prod-wus2-001" \
--remote-vnet "vnet-hub-prod-wus2-001" \
--allow-vnet-access --allow-forwarded-traffic --use-remote-gateways
After peering, link the spoke to name resolution so its resources resolve private endpoints and on-premises names the same way the rest of the estate does, either by linking the private DNS zones to the spoke or by relying on the Private DNS Resolver ruleset if that is how you wired resolution. Expected result: a workload network that reaches the hub, reaches on-premises through the hub’s gateway, resolves names correctly, and reports to the central workspace, all without a new decision. Failure mode to watch: setting gateway transit on the hub peering but forgetting use-remote-gateways on the spoke peering, which leaves the spoke unable to reach on-premises even though the tunnel is up.
Step 5: The monthly health and drift checks
The operating loop is a short list you run on a cadence, and every item is a query the foundation already answers. Read the policy compliance summary to see what has drifted, read the Secure Score for its trend rather than a target, list the exemptions to catch any waiver that quietly outlived its removal date, and read Advisor’s cost findings for the idle and oversized resources worth cleaning up. None of this is heavy; the point is that it happens on a schedule instead of when something has already gone wrong.
# Policy compliance: what has drifted from the guardrails
az policy state summarize --subscription "PROD_SUB_ID" \
--query "results.policyAssignments[].{assignment:policyAssignmentId, nonCompliant:results.nonCompliantResources}" -o table
# Secure Score: read the trend, not a target
az security secure-scores show --name "ascScore" \
--query "{current:score.current, max:score.max, percentage:score.percentage}" -o table
# Exemptions: catch any waiver that outlived its expiry
az policy exemption list --scope "/providers/Microsoft.Management/managementGroups/mg-northfork" \
--query "[].{name:name, category:exemptionCategory, expires:expiresOn}" -o table
# Advisor cost findings: idle and oversized resources
az advisor recommendation list --category Cost -o table
A worked example
Northfork runs the loop in about an hour on the first of the month. It reads cost by CostCenter, sees the file-server workload creeping up, and finds an oversized disk in Advisor to trim. Its IT budget alerted at eighty percent once, in the month a project spun up test machines, which is exactly when the alert is supposed to fire. When a developer joined who needed a sandbox that could not touch production, Northfork ran the vending checklist: created the subscription, dropped it into the Non-Production management group, watched the compliance flood evaluate and settle, assigned the admin group Contributor, and confirmed its logs reached the central workspace, all in an afternoon and all governed from the first minute. When a new line-of-business application needed its own network, Northfork ran the add-a-spoke runbook, peered a 10.40.3.0/24 spoke to the hub, and the application came up logging and tagged and resolving without anyone configuring those things, because the tree already enforced them. Nothing in either growth event required moving what already existed.
Naming convention
Name budgets for what they cap, budget-costcenter-it, so the budget list reads as a set of team ceilings. Name spokes and their peerings by the convention the network was built on, vnet-spoke-app-prod-wus2-001 and hub-to-spoke-app, so the topology stays legible as it grows. Keep the operating loop itself written down, the monthly checklist and who runs it, in the same document as the rest of the foundation, because the loop only protects you if it actually runs, and a checklist with an owner and a date is the difference between operating the estate and merely intending to.
The end-to-end test
Prove the operating loop actually produces signal rather than just existing on paper. The cleanest single proof is the compliance and posture query returning real numbers: run the policy compliance summary and the Secure Score query and confirm both come back populated, which means the foundation is answering the operating questions on demand. For a live proof of the alerting path, set a deliberately tiny test budget so its threshold trips within the cycle, confirm the action group notifies you, and then remove the test budget. A loop that returns current compliance, a current score, and a budget alert you actually received is an operating loop that works, as opposed to a set of good intentions nobody has tested.
# Prove the signals are live: compliance summary and Secure Score both return
az policy state summarize --subscription "PROD_SUB_ID" --query "results.nonCompliantResources" -o tsv
az security secure-scores show --name "ascScore" --query "score.percentage" -o tsv
# Expect: a compliance number and a score percentage. Optionally, a short-lived test budget
# with a tiny amount to confirm the action group actually pages you, then delete it.
Before you call it done
- Tag inheritance enabled in Cost Management, and cost analysis grouped by CostCenter returning per-team spend.
- Budgets scoped by tag at subscription level, with 80% and 100% alerts wired to the operations action group.
- A subscription-vending checklist written down, with the management-group placement as the step that inherits the tree.
- An add-a-spoke runbook that peers into the hub with gateway transit and remote-gateway use, links name resolution, and lets policy and diagnostics inherit.
- The monthly loop defined: compliance summary, Secure Score trend, exemption expiry review, Advisor cost findings.
- End-to-end test passed: compliance and Secure Score queries return current numbers, and a test budget alert was received.
That completes the foundation and this series. You poured the tree, the network, identity, the record, and the guardrails, in that order and for that reason, and you now have an estate you can operate in an hour a month and grow for years by placement rather than rebuild. The concrete is set, the house is standing, and every future thing you add already has somewhere to go.
Azure Landing Zones
‹ Previous: [LZ 8] Grows by Placement, Not Rebuild




