Finance sent a spreadsheet. Azure was up 38 percent month over month. The landing zone looked finished. Hub, spokes, policy, logging. Nobody could say which team caused the jump.
Someone had already tried to help. A cost cleanup the quarter before deleted a subscription tagged non-production. That was the sandbox Log Analytics workspace from the logging article. The invoice dipped for a month. The next 504 ate a night because the firewall was still writing into a hole.
Cost work that only deletes things makes you cheaper and darker at the same time.
This is article eight in the Azure Landing Zone series. Subscriptions were supposed to be the billing boundary. Policy was supposed to force tags. Logging is already eating gigabytes you can see in the Usage table. Cost is whether any of that shows up as a name a human will own.
The bill is a design document
You already drew the seams. Subscription design is a billing decision wearing an architecture hat. Management groups are where a budget can sit above a team that keeps minting subscriptions. Policy is the only reason a tag exists on the resource that got created from the portal on Friday.
Cost Management only reports against those seams. It will not invent a cost center you refused to require.
Shared subscriptions turn chargeback into a meeting. Optional CostCenter values group as Other. And if the hub firewall bill sits in the platform subscription with no allocation, app teams will keep stuffing more traffic through it and treat inspection as free.
I still walk into estates where FinOps has a slide and the platform team has a credit card. The slide has a tagging standard. The credit card has unattached disks, Traffic Analytics on every VNet, and a workspace retaining 365 days hot because nobody wanted to argue with security.
Platform pays for shared. Workloads pay for theirs.
Write the split down. Put it in the same repo as the hub module, not in a Confluence page that dies at the next reorg.
Platform owns the things every spoke uses whether the app team knows it or not: hub VNet, Azure Firewall, ExpressRoute, private DNS, the platform Log Analytics workspace, the DINE assignments, Defender on the control plane. Those invoices belong on a platform cost center. Split them later if finance needs showback. Do not pretend a spoke “doesn’t use the firewall” because they never opened a ticket.
Workloads own the spoke, the SKUs they picked, their disks, their private endpoints, and the log volume they generate. Console logging on App Service, Container Insights at default verbosity, every diagnostic category on Storage: that is their bill even if the destination workspace is yours.
The messy line is ingestion. One platform workspace, a dozen teams dumping data into it. If you cannot attribute GB to a resource or a tag, the platform team eats every “all logs” checkbox a vendor ticked on a Friday. Table-level RBAC from the logging post keeps people out of firewall rows. It does not stop them from filling the workspace.
A second workspace for noisy app telemetry is allowed if you mean it. Prod vs non-prod is a real split. One workspace per team is how you got the deleted sandbox.
Four tags, then deny
Cost Management grouping, the invoice export, and Azure Resource Graph should use the same four tags: Environment, CostCenter, Owner, Application. That is enough to answer who, what, and whether it should still exist. A twelfth tag that only exists in a wiki will not.
Inherit from the resource group. Deny at the management group if any of the four is missing. Append is how you get a resource that looks tagged and still groups as “Other” because the value is empty.
Terraform default_tags on the provider covers what the pipeline creates. It does not cover the Key Vault someone stood up in the portal. You already know that pattern from diagnostics. Same fix: policy at the management group, deny on create, a one-time remediation that actually writes values instead of checking a box.
provider "azurerm" { features {} default_tags { tags = { Environment = var.environment CostCenter = var.cost_center Owner = var.owner Application = var.application } }}
If finance wants a fifth tag next quarter, add it to the policy initiative first, then to the module. Do not add it to a spreadsheet and hope.
Budgets before the invoice
A budget with no action group is a dashboard. Put a consumption budget on every subscription the landing zone creates. Alert the Owner mailbox and the platform distribution list at 50, 80, and 100 percent. The 50 percent mail is the one that catches Traffic Analytics before month-end.
resource "azurerm_consumption_budget_subscription" "spoke" { name = "budget-${var.subscription_name}" subscription_id = var.subscription_id amount = var.monthly_budget_usd time_grain = "Monthly" time_period { start_date = var.budget_start } notification { enabled = true threshold = 80.0 operator = "GreaterThan" contact_emails = [var.owner_email, var.platform_email] contact_groups = [azurerm_monitor_action_group.cost.id] }}
Management group budgets catch the subscription nobody remembered to wrap. Shared subscriptions need a filter on CostCenter or you will page the wrong team.
Do not set the amount to whatever last month cost plus a prayer. Set it to what the design said the spoke should cost, then explain the overage. If it fires every month, nobody reads it. If it never fires, you set it after the fact.
Logging will pick this fight
You already ran this from the Kusto companion. Run it again as a cost query, not an incident query.
Usage| where TimeGenerated > ago(24h)| where IsBillable == true| summarize IngestionGB = round(sum(Quantity) / 1000, 2) by DataType| order by IngestionGB desc
AZFWNetworkRule at a few GB is the hub doing its job. ContainerLog or AppServiceConsoleLogs at hundreds of GB a day is a verbosity setting, not a landing zone requirement. AzureDiagnostics still huge after you moved to resource-specific tables means you are paying twice.
“All categories” on Storage and Key Vault feels responsible. Query the categories you used in the last three incidents and enable those. Flow logs belong in storage unless Traffic Analytics is a decision you can point at. NSG flow logs you forgot to turn off after Virtual Network flow logs went in will bill you for both until someone notices.
retention_in_days = 30 is a billing default. Ninety days hot plus archive is an incident-response default. 365 days hot on a workspace that already has archive configured is how the logging bill outruns the compute bill.
Do not delete the workspace to save money. Change the categories. Change retention. Charge the workload for volume you can attribute. The cleanup that deleted sub-sandbox-logs already taught you the other version of this story.
The resources that never made the architecture review
Cost Management’s “idle” views miss things that still look deployed.
Look at unattached disks from a VM rebuild, public IPs on a load balancer that got replaced, leftover NICs, snapshots from a migration that shipped six months ago. VMs in Stopped (allocated) because someone hit stop in the portal instead of deallocate. Standard SKU load balancers and firewalls that bill even when traffic is quiet.
Resources| where type == "microsoft.compute/disks"| where properties.diskState == "Unattached"| project name, resourceGroup, subscriptionId, skuName=sku.name, sizeGB=properties.diskSizeGB, tags
Run the same idea for public IPs with ipConfiguration empty, and for snapshots older than your recovery policy. Tag-based cleanup that only deletes Environment=nonprod will hit the wrong subscription again. Query the actual waste, then delete what the query named.
SKU policy belongs here too. limit_vm_sizes and approved_skus_only were already in the policy initiative. If those assignments are Audit, the bill is the enforcement. Flip the ones that matter to Deny before you ask finance why D-series showed up in a landing zone that standardized on B and E.
If it is not in Terraform, it will drift
Skip the budget resource and the next subscription ships without one. Skip default_tags and portal resources show up untagged until someone files a waiver. Skip the deny policy and Cost Management will keep grouping spend as Other.
The next numbered article is the deployment model: modules, pipelines, who is allowed to click. Cost rules that live in a wiki will not survive that any better than hub networking did.
If finance cannot name a cost center for the firewall, or Usage says one table is half the workspace, you already know what to fix. Turning logging off, deleting the sandbox, and leaving SKU policy on Audit are the same move. They make the invoice quieter. They do not make the landing zone cheaper to run.


