Six Azure Tags That Make Chargeback and Showback Actually Work
2025-10-16 · ~6 min read
One tag can’t tell the whole story. Here’s how I built a practical Azure tagging model that connects operations, finance, and security — and finally made chargeback for shared services accurate and defensible.
The story (from the trenches)
When Finance first asked for a chargeback report by Line of Business, I thought our tags would make it easy.
Every resource had Owner, Application, and CostCenter — what could go wrong?
Everything, it turns out.
Our tags told us who ran the resource, not who paid for it.
So I rebuilt the model from the ground up using six simple tags that finally made Azure Policy, FinOps, and security data line up.
Owner: Data
Application: Platform Services
CostCenter: 200-1234
Type: Server
DomainJoined: Yes
ChargebackType: Shared
It looked clean, but it told the wrong story — who operates it, not who funds it.
The minimal tag set that actually works
| Tag | Example | Why it matters |
|---|---|---|
| Owner | Data |
Operational custodian. Who runs/fixes the thing. |
| Application | Platform Services |
Workload identity for slicing costs and incidents. |
| CostCenter | 200-1234 |
The Line of Business that funds/consumes it. Drives showback/chargeback. |
| Type | Server / Desktop / Appliance |
Inventory posture & lifecycle (patching, backup, decomm). |
| DomainJoined | Yes / No |
Security & compliance signal (identity plane, GPO reach, break-glass lists). |
| ChargebackType | Shared / Direct |
Flags resources that need split or direct billing. |
Key idea:
Owneris not the payer.CostCenteris the money tag.Applicationis the bridge between Ops (Owner) and Finance (CostCenter).TypeandDomainJoinedmake your inventory and patching views accurate.
Shared services: how we made chargeback fair
Shared platforms (databases, monitoring, identity, logging) don’t map to a single payer. We used ChargebackType=Shared to mark them and cost allocation rules to split costs fairly among consuming CostCenters.
Example split (concept):
| Source (tag) | Target CostCenter | Allocation % |
|---|---|---|
ChargebackType=Shared for Application=Platform Services |
200-1234 |
40% |
| " | 300-5678 |
30% |
| " | 400-6789 |
30% |
Start with showback (no internal billing) for a few cycles to build trust. Move to chargeback once your tag coverage is stable.
Queries you’ll use every week
1) Find resources missing critical tags
Resources
| extend Owner=tostring(tags.Owner), App=tostring(tags.Application), CC=tostring(tags.CostCenter), Type=tostring(tags.Type), DJ=tostring(tags['DomainJoined'])
| where isempty(Owner) or isempty(App) or isempty(CC) or isempty(Type) or isempty(DJ)
| project subscriptionId, resourceGroup, name, type, Owner, App, CC, Type, DJ
| order by subscriptionId, resourceGroup, name
2) Showback view by Line of Business (CostCenter) and Application
CostByResource
| summarize CostUSD=sum(AmortizedCost) by CostCenter, Application, bin(UsageDate, 1d)
| order by UsageDate asc
3) Patch & compliance slice by Type and DomainJoined
Resources
| where type =~ 'microsoft.compute/virtualmachines'
| extend Type=tostring(tags.Type), DJ=tostring(tags['DomainJoined'])
| summarize CountVMs=count() by Type, DJ
4) Owner ≠ Payer sanity check
Resources
| extend Owner=tostring(tags.Owner), App=tostring(tags.Application), CC=tostring(tags.CostCenter)
| summarize Resources=count() by Owner, CC
| order by Owner, CC
Azure Policy: deny on missing tags (with friendly remediation)
Require the six tags on create. Deny if missing; pair with an initiative that adds a Modify policy for auto-append in well-known RGs.
{
"properties": {
"displayName": "Require core tags on resources",
"policyRule": {
"if": {
"anyOf": [
{ "field": "tags['Owner']", "exists": false },
{ "field": "tags['Application']", "exists": false },
{ "field": "tags['CostCenter']", "exists": false },
{ "field": "tags['Type']", "exists": false },
{ "field": "tags['DomainJoined']", "exists": false },
{ "field": "tags['ChargebackType']", "exists": false }
]
},
"then": { "effect": "deny" }
}
}
}
Tip: keep tag names exact (
DomainJoined, notIs Domain Joined). If you’re already live with variants, standardize and migrate with a one-time script.
PowerShell: weekly tag health & nudge
Connect-AzAccount
$query = @"
Resources
| extend Owner=tostring(tags.Owner), App=tostring(tags.Application), CC=tostring(tags.CostCenter), Type=tostring(tags.Type), DJ=tostring(tags['DomainJoined']), CB=tostring(tags.ChargebackType)
| where isempty(Owner) or isempty(App) or isempty(CC) or isempty(Type) or isempty(DJ) or isempty(CB)
| project subscriptionId, resourceGroup, name, type, Owner, App, CC, Type, DJ, CB
"@
$subs = (Get-AzSubscription).Id
$arg = @{ Query = $query; Subscriptions = $subs }
$result = Search-AzGraph @arg
$result | Export-Csv -NoTypeInformation -Path "Tag-Remediation-MissingCoreTags.csv"
Operating model that stuck
- Designate tag owners: each Application has a named owner responsible for fixing missing tags.
- Showback first: publish CostCenter/Application dashboards monthly; fix gaps.
- Promote to chargeback: once untagged cost <2%, switch Finance to these numbers.
- Hold the line: deny on create, auto-append only in controlled landing zones.
TL;DR
Ownertells us who runs it.CostCentertells us who pays.Applicationties the story together.- Add
Type,DomainJoined, andChargebackTypeto unlock patching, inventory, and FinOps accuracy. - Mark shared services and split fairly. Start with showback, then charge back.
This six-tag model is the smallest practical set that made our governance, security, and FinOps reporting finally agree with each other — and with Finance.