Azure Tagging Best Practices 2025: The $2.3M Hidden Cost of Tag Variations
2025-11-30 · ~30 min read · Updated 2025-12-06
One tag key. 247 different spellings. $2.3M in cost allocation failures. Real lessons from enterprise Azure tag governance: why Azure Policy isn't enough and the automation that finally stopped tag chaos across 31,000 resources.
The $2.3M Tag Mess: What Happens When 78002566 Becomes 247 Different Variations
This guide is part of our AI-Assisted Azure Operations hub exploring how AI tools transform cloud administration and productivity workflows.
The Finance Call That Changed Everything
February 2023. Conference call with the CFO.
CFO: "We spent $2.3 million on Azure last year. Show me the breakdown by department."
Me: "Sure, I'll pull the Cost Center tags..."
I opened Azure Cost Management. Filtered by tag: CostCenter
The results:
- CostCenter: 78002566
- CostCenter: 9807566
- CostCenter: cost-center-78002566
- CostCenter: CC-78002566
- CostCenter: 78002566-prod
- CostCenter: 78OO2566 (that's two letter O's, not zeros)
789 resources. 247 different variations of the same cost center.
The CFO was still on the line. Waiting.
I had no answer.
When Tags Become Tag Chaos
Microsoft's documentation makes tagging sound simple:
1. Define your tags
2. Apply them to resources
3. Generate cost reports
What they don't tell you:
Cost Centers Become Creative Writing
One cost center. 247 ways to spell it:
78002566 ↠Correct
9807566 ↠Missing first digit
cost-center-78002566 ↠Someone added prefix
CC-78002566 ↠Abbreviation
78002566-prod → Someone added environment
78OO2566 ↠Letter O instead of zero (I/O confusion)
78002566_PROD ↠Underscore instead of hyphen
cc78002566 ↠Lowercase no separator
Cost Center: 78002566 ↠Friendly format with space
(78002566) ↠Someone put it in parentheses
I found all of these. In production. On billable resources.
Applications Become a Free-for-All
Active Directory alone had 8 variations:
Active Directory
active directory
ActiveDirectory
AD
Active Dir
Microsoft Active Directory
MSFT AD
ad-prod
Rubrik backup appliance had 6:
Rubrik
rubrik
Rubrik Backup
Rubrik-Appliance
rubrik-backup-appliance
RUBRIK
Even Notepad (yes, someone tagged a VM "Notepad"):
Notepad
notepad
Windows Notepad
notepad.exe
Owners Turn Into Organizational Charts
Who owns this resource? Nobody knows:
Data ↠Department
data ↠Lowercase department
Data Team ↠Informal name
Data Engineering ↠Sub-team
DataEngineering ↠No space
DE ↠Abbreviation
Marketing ↠Different department
Development ↠Another department
Dev ↠Abbreviated
John Smith ↠Person, not team
john.smith@company.com ↠Email instead of team
jsmith ↠Username
One resource had owner: "TBD"
It had been running for 14 months. $11,000 in costs. Nobody knew who owned it.
Environments Multiply Like Rabbits
Production alone had 12 spellings:
Production
Prod
PROD
production
prod
Vmware-Prod ↠Someone added infrastructure type
Production-East ↠Someone added region
Prod-Primary ↠Someone added DR status
PRODUCTION
Production_1 ↠Numbered for some reason
Prd ↠Creative abbreviation
P ↠Just... why?
Full environment tag chaos:
Production, Prod, PROD, production, Vmware-Prod
Development, Dev, DEV, dev, Vmware-Dev, development
Sandbox, sandbox, SandBox, SANDBOX
QA, Qa, qa, Quality, quality, TEST
That's 22+ ways to say "not production."
Types Lose All Meaning
Server. Just... server:
Server
server
APPLICATION SERVER
Application Server
Web Server
web-server
Database Server
db-server
File Server
SQL Server ↠That's a product, not a type
Server-VM ↠Redundant
virtual-server ↠Everything in Azure is virtual
The entire Type tag catalog:
Appliance, appliance, Virtual Appliance, Network Appliance
Server, server, Virtual Server, Physical Server (in Azure??)
Desktop, desktop, Virtual Desktop, VDI, AVD
Workstation, workstation, Dev Workstation
What This Actually Costs
Time Waste (Monthly)
Me:
- 6 hours manually sorting Azure costs in Excel
- 2 hours fixing tag typos
- 3 hours in meetings explaining why reports don't match
Finance team:
- 4 hours reconciling Azure bills against departments
- 2 hours chasing down resource owners
- 1 hour creating manual pivot tables
Department managers:
- 2 hours arguing about cost allocations
- "That's not our server!"
- "We didn't deploy that!"
Total: 20 hours/month of wasted labor. Across multiple people.
Real Dollar Impact
Direct costs:
- $2.3M annual Azure spend
- Can't track by department
- Can't identify waste
- Can't forecast accurately
- Can't do chargeback
The $180K mystery:
One line item in our Azure bill: "Untagged resources - $180,047"
We spent 3 days tracking it down.
Turned out: Dev team's test environment for a project that ended 14 months ago.
Nobody knew it was still running.
$180K. Gone. Because someone forgot to add tags.
The Breaking Point
CFO (in leadership meeting): "If we can't track Azure costs by department like we do with on-premises, we're moving everything back to the datacenter."
CIO: "Give us 90 days to fix it."
That's when I got the assignment.
What I Built Instead
I created a three-tier tag governance system with Azure Policy enforcement.
Tier 1: Required Tags (Enforced by Azure Policy)
These tags are MANDATORY. Deployments fail without them.
CostCenter
- Format: Exactly 8 numeric digits
- Valid:
78002566 - Invalid:
cost-center-78002566,CC-78002566,78OO2566,9807566 - Enforcement: Regex pattern
^[0-9]{8}$
Azure Policy:
{
"mode": "Indexed",
"policyRule": {
"if": {
"allOf": [
{
"field": "type",
"in": [
"Microsoft.Compute/virtualMachines",
"Microsoft.Storage/storageAccounts",
"Microsoft.Network/virtualNetworks"
]
},
{
"not": {
"field": "tags['CostCenter']",
"match": "^[0-9]{8}$"
}
}
]
},
"then": {
"effect": "deny"
}
}
}
What this does: Blocks any VM, storage account, or VNet deployment without a valid 8-digit cost center.
Environment
- Format: Enum (exactly 4 allowed values)
- Valid:
Production,Development,QA,Sandbox - Invalid:
Prod,prod,PROD,Vmware-Prod,Dev,development - Enforcement: Exact match required
Azure Policy:
{
"mode": "Indexed",
"policyRule": {
"if": {
"allOf": [
{
"field": "type",
"equals": "Microsoft.Compute/virtualMachines"
},
{
"not": {
"field": "tags['Environment']",
"in": ["Production", "Development", "QA", "Sandbox"]
}
}
]
},
"then": {
"effect": "deny"
}
}
}
The conversation this forces:
Developer: "I need to deploy a test VM."
Azure Policy: "Deployment denied. Tag 'Environment' must be one of: Production, Development, QA, Sandbox"
Developer: "Fine. I'll use 'Development'."
Tag governance: Working as designed.
Owner
- Format: Department name from approved list
- Valid:
Data,Marketing,Development,Infrastructure,Security - Invalid:
data,John Smith,john.smith@company.com,TBD,jsmith - Enforcement: Enum of approved departments
Azure Policy:
{
"mode": "Indexed",
"policyRule": {
"if": {
"allOf": [
{
"field": "type",
"equals": "Microsoft.Compute/virtualMachines"
},
{
"not": {
"field": "tags['Owner']",
"in": [
"Data",
"Marketing",
"Development",
"Infrastructure",
"Security",
"Finance",
"operations"
]
}
}
]
},
"then": {
"effect": "deny"
}
}
}
Why this matters: No more "Owner: TBD" or email addresses. Clean department ownership.
Tier 2: Strongly Recommended (Audit Mode)
These tags trigger warnings but don't block deployments.
Application
- Format: Title Case, official product name from CMDB
- Valid:
Active Directory,Rubrik,SQL Server,Notepad - Invalid:
active directory,AD,ad-prod,rubrik-backup - Enforcement: Audit policy (warns but allows)
Why not enforce? Applications change. New products deploy. Strict enforcement here would block legitimate work.
Policy effect: Audit (creates compliance report, doesn't block)
Type
- Format: Enum from approved list
- Valid:
Appliance,Server,Desktop - Invalid:
server,Virtual Server,APPLICATION SERVER - Enforcement: Audit only
Approved types:
- Server - General purpose compute
- Appliance - Vendor virtual appliances (Rubrik, F5, etc)
- Desktop - Virtual desktop infrastructure (AVD)
Tier 3: Optional (No Enforcement)
These are nice-to-have but not enforced:
Project- Which project funded this resourceDataClassification- Public, Internal, Confidential, RestrictedBackup- Yes/No (does this need backups?)MaintenanceWindow- When can we patch this?
Why optional? Not every resource needs these. Don't block deployments for metadata that's not critical.
Before You Fix Governance: The Survival Query
If you haven't implemented tag governance yet, here's how to survive in the meantime.
This is the query I used while waiting for leadership approval to implement tag governance. It handles all the case variations using coalesce():
Resources
| where type =~ 'microsoft.compute/virtualmachines'
| extend vmSize = tostring(properties.hardwareProfile.vmSize)
| extend osType = tostring(properties.storageProfile.osDisk.osType)
| extend powerState = tostring(properties.extended.instanceView.powerState.code)
| extend createdTime = tostring(properties.timeCreated)
| extend nicId = tostring(properties.networkProfile.networkInterfaces[0].id)
// Extract OS version information
| extend osVersion = tostring(properties.storageProfile.imageReference.exactVersion)
| extend osSku = tostring(properties.storageProfile.imageReference.sku)
| extend osOffer = tostring(properties.storageProfile.imageReference.offer)
| extend osPublisher = tostring(properties.storageProfile.imageReference.publisher)
| extend osVersionDisplay = strcat(osPublisher, ' ', osOffer, ' ', osSku)
| join kind=leftouter (
Resources
| where type =~ 'microsoft.network/networkinterfaces'
| extend privateIp = tostring(properties.ipConfigurations[0].properties.privateIPAddress)
| project nicId = id, privateIp
) on $left.nicId == $right.nicId
| join kind=leftouter (
ResourceContainers
| where type == 'microsoft.resources/subscriptions'
| project subscriptionId, subscriptionName = name
) on subscriptionId
| extend ipAddress = iff(powerState == 'PowerState/running', privateIp, 'N/A')
// Extract specific tags (handles case variations)
| extend Application = coalesce(tags.Application, tags.application, tags.APPLICATION, 'Not Tagged')
| extend Owner = coalesce(tags.Owner, tags.owner, tags.OWNER, 'Not Tagged')
| extend Type = coalesce(tags.Type, tags.type, tags.TYPE, 'Not Tagged')
| extend Environment = coalesce(tags.Environment, tags.environment, tags.ENVIRONMENT, 'Not Tagged')
| project
id,
name,
subscriptionId,
subscriptionName,
resourceGroup,
location,
vmSize,
osType,
osVersionDisplay,
powerState,
createdTime,
ipAddress,
Application,
Owner,
Type,
Environment
What This Query Does
The critical lines are these:
| extend Application = coalesce(tags.Application, tags.application, tags.APPLICATION, 'Not Tagged')
| extend Owner = coalesce(tags.Owner, tags.owner, tags.OWNER, 'Not Tagged')
| extend Type = coalesce(tags.Type, tags.type, tags.TYPE, 'Not Tagged')
| extend Environment = coalesce(tags.Environment, tags.environment, tags.ENVIRONMENT, 'Not Tagged')
What coalesce() does:
1. Try tags.Application (Title Case)
2. If not found, try tags.application (lowercase)
3. If not found, try tags.APPLICATION (UPPERCASE)
4. If none exist, return 'Not Tagged'
This handles:
- "Application", "application", "APPLICATION" - all three case variations
- "Owner", "owner", "OWNER" - all three case variations
- "Environment", "environment", "ENVIRONMENT" - all three case variations
- Missing tags - shows "Not Tagged" instead of blank cells
Why This Is a Band-Aid, Not a Fix
The problem with this approach:
Every time someone invents a NEW variation, you need to update the query:
- Someone uses "app" instead of "Application"? Update the query.
- Someone uses "Env" instead of "Environment"? Update the query.
- Someone uses "Own" instead of "Owner"? Update the query.
I was updating this query every 2-3 weeks as people found creative new ways to spell tags.
The real problem:
- Your query gets longer and longer
- You're fighting symptoms, not the disease
- Finance still gets inconsistent reports
- New variations appear faster than you can add them
What Tag Governance Does Instead
With tag governance policies in place, this query becomes:
Resources
| where type =~ 'microsoft.compute/virtualmachines'
| extend Application = tostring(tags.Application)
| extend Owner = tostring(tags.Owner)
| extend Type = tostring(tags.Type)
| extend Environment = tostring(tags.Environment)
No coalesce(). No workarounds. No variations.
Because Azure Policy enforces exact case and format BEFORE deployment.
Developer tries to deploy with "application" (lowercase)?
Deployment denied.
Tag 'Application' not found.
Valid tags must use Title Case.
They fix it. Deploy succeeds. Your query works. Forever.
Use This Query While You Wait
If you can't implement tag governance immediately:
- Save this query - Bookmark it in Azure Resource Graph Explorer
- Use it for reports - Better than nothing
- Share the pain - Show leadership how many
coalesce()calls you need - Build the business case - "We're spending 6 hours/month working around tag chaos"
But understand: This is a temporary survival tactic, not a long-term solution.
Tag governance eliminates the need for this workaround entirely.
The Deployment Plan
Week 1: Audit Phase
Run this KQL query to find all your tag variations:
Resources
| where isnotempty(tags)
| mv-expand bagexpansion=array tags
| extend tagKey = tostring(bag_keys(tags)[0])
| extend tagValue = tostring(tags[tagKey])
| summarize
ResourceCount = count(),
Example = any(name)
by tagKey, tagValue
| where tagKey in ("CostCenter", "Environment", "Owner", "Application", "Type")
| order by tagKey, ResourceCount desc
What you'll find:
- All the creative spellings
- All the typos
- All the "someone thought this was a good idea" moments
Save this output. You'll need it to build your cleanup script.
Week 2: Policy Creation (Audit Mode First!)
Critical: Start with audit mode. Not deny mode.
{
"then": {
"effect": "audit" // ↠Start here, not "deny"
}
}
Why? You need to see what would break before you enforce.
Deploy policies:
1. CostCenter validation (audit)
2. Environment enum (audit)
3. Owner enum (audit)
Let it run for 2 weeks in audit mode.
Check the compliance dashboard daily. See what's non-compliant. Talk to teams. Understand the edge cases.
Week 3-4: Cleanup Phase
Write remediation script for existing resources:
# Get all VMs with invalid CostCenter tags
$vms = Search-AzGraph -Query @"
Resources
| where type == "microsoft.compute/virtualmachines"
| where tags['CostCenter'] !matches regex @'^[0-9]{8}$'
| project name, resourceGroup, subscriptionId,
CurrentTag = tostring(tags['CostCenter'])
"@
foreach ($vm in $vms) {
Write-Host "VM: $($vm.name) has invalid CostCenter: $($vm.CurrentTag)"
# Extract digits only
$cleanedTag = $vm.CurrentTag -replace '[^0-9]', ''
# Pad to 8 digits if needed
if ($cleanedTag.Length -lt 8) {
Write-Warning "CostCenter too short, needs manual review"
continue
}
if ($cleanedTag.Length -gt 8) {
$cleanedTag = $cleanedTag.Substring(0, 8)
}
Write-Host " Updating to: $cleanedTag"
# Update the tag
Update-AzTag -ResourceId $vm.id -Tag @{CostCenter = $cleanedTag} -Operation Merge
}
Work with teams to fix non-compliant resources:
- Email resource owners
- Provide the correct tag values
- Give them 2 weeks to fix
Week 5: Enforcement
Switch policies from "audit" to "deny":
{
"then": {
"effect": "deny" // ↠Now we enforce
}
}
What happens:
Developer tries to deploy VM without valid CostCenter:
Error: Resource 'vm-test-001' was disallowed by policy.
Policy: Require-CostCenter-Tag
Details: Tag 'CostCenter' must match pattern ^[0-9]{8}$
They add the tag. Deployment succeeds. Tag governance: working.
Week 6: Victory
Generate your first accurate cost report:
Resources
| where type == "microsoft.compute/virtualmachines"
| extend CostCenter = tostring(tags['CostCenter'])
| extend Owner = tostring(tags['Owner'])
| extend Environment = tostring(tags['Environment'])
| summarize VMCount = count() by CostCenter, Owner, Environment
| order by Owner, Environment
Export to Excel. Send to finance.
CFO response: "This is exactly what I needed. Thank you."
Meeting time: 3 minutes. Previously: 6 hours of manual Excel work.
Three Months Later: The Results
Before Tag Governance
- 789 resources deployed
- 247 different CostCenter variations
- 6 hours/month cleaning data manually
- Finance threatening to kill Azure program
- No accurate department cost allocation
- $180K in untracked "mystery" resources
After Tag Governance
New deployments:
- 100% compliant (policy enforces it)
- Zero tag variations
- Instant cost allocation
Old resources:
- Remediation script cleaned 95% automatically
- 5% required manual review (genuinely ambiguous cases)
- 100% tagged within 30 days
Finance reports:
- Automated
- Accurate
- Generated in 3 minutes instead of 6 hours
- Department breakdown matches budget structure
The CFO call (Round 2):
CFO: "Show me Q2 costs by department."
Me: [Clicks button in Azure Cost Management]
"Data: $478K, Marketing: $312K, Development: $1.2M, Infrastructure: $287K, Security: $156K"
CFO: "Perfect. Send this to the leadership team."
Meeting: 3 minutes.
Previously: 6 hours of manual Excel hell.
The Tag Governance Starter Kit
I've packaged everything into a deployable kit.
What's Included
- Azure Policy Definitions (JSON files)
- CostCenter validation policy
- Environment enum policy
- Owner enum policy
- Application audit policy
-
Combined policy initiative
-
Tag Standards Document (Markdown)
- What each tag means
- Valid formats and examples
- Invalid formats (what to avoid)
-
Approved enums for each tag
-
Remediation PowerShell Script
- Scans all resources for invalid tags
- Auto-corrects common mistakes
- Flags ambiguous cases for manual review
-
Bulk update capability
-
Finance Report Template (Power BI)
- Cost breakdown by department
- Cost breakdown by environment
- Resource count by owner
-
Tag compliance dashboard
-
Communication Plan (Email templates)
- Announcement to organization
- Developer quick-start guide
- FAQ document
- Escalation process
Download
Repository: https://github.com/azure-noob/tag-governance-kit
Files:
tag-governance-kit/
├── policies/
│ ├── costcenter-validation.json
│ ├── environment-enum.json
│ ├── owner-enum.json
│ └── tag-governance-initiative.json
├── scripts/
│ ├── audit-tags.ps1
│ ├── remediate-tags.ps1
│ └── generate-tag-report.ps1
├── docs/
│ ├── TAG-STANDARDS.md
│ ├── DEPLOYMENT-GUIDE.md
│ └── FAQ.md
├── templates/
│ └── cost-allocation-report.pbix
└── README.md
Clone it. Customize it. Deploy it.
🔍 Find the "Shadow" Resources (No Tags)
The most dangerous resources isn't the one with the misspelled tag—it's the one with no tags.
// Find "Shadow IT" resources (Older than 30 days, Zero tags)
Resources
| where tags == "{}" or isnull(tags)
| where todatetime(properties.timeCreated) < ago(30d)
| project name, resourceGroup, subscriptionId, location, properties.timeCreated
🛑 You Can't Tag What You Don't Own
Tag governance fails when no one is responsible for enforcing it.
Download the Azure RACI Matrix and assign the 'Governance Enforcer' role today.
The One Thing Microsoft Won't Tell You
Tag Governance Comes BEFORE Deployment
Microsoft's docs show you how to tag.
They don't tell you:
- Tag chaos happens FAST (247 variations in 18 months)
- Fixing tags retroactively is painful
- Finance will use "we can't track costs" to kill cloud projects
- One typo ("78OO2566" instead of "78002566") can hide thousands in spending
- Lowercase vs uppercase creates separate categories in cost reports
- Abbreviations destroy cost allocation accuracy
The rule I learned the hard way:
If you can't answer "Who pays for this resource?" in 3 seconds, you have a tag problem.
Fix it before you deploy resource #100.
By resource #1,000, it's too late.
By resource #10,000, you're in tag hell.
The Real Problem
Tags aren't a technical problem. They're an organizational problem.
You're not fighting Azure. You're fighting:
- Different teams with different naming conventions
- Developers who want to deploy fast
- Finance who needs accurate cost allocation
- Security who needs owner accountability
- Compliance who needs data classification
Tag governance forces these conversations to happen BEFORE deployment.
That's uncomfortable.
But it's cheaper than spending $180K on resources nobody knows about.
What This Prevents
Disasters This Tag Governance Avoids
✅ Finance revolts - When they can't track $2.3M in spending
✅ Cost allocation wars - "That's not our server!" (yes it is, the tag says so)
✅ Audit failures - Can't prove resource ownership for SOC2/ISO compliance
✅ Budget overruns - Can't identify waste by department to cut
✅ Cloud repatriation - CFO threatening to move back to on-prem
✅ Manual reporting hell - 6 hours/month of Excel gymnastics
✅ Mystery resources - The $180K test environment nobody knew about
✅ Chargeback impossibility - Can't bill departments for their usage
✅ Tag sprawl - 247 variations of the same cost center
✅ Leadership distrust - "If you can't track basic costs, what else don't you know?"
Real Stories From The Field
Company A: Spent $340K on "untagged resources" over 2 years. Turned out to be abandoned dev environments. Tag governance would have caught this in month 1.
Company B: Finance refused to approve cloud budget increase because they couldn't verify department spending. Tag governance gave them the breakdown they needed. Budget approved.
Company C: SOC2 auditor asked "Who owns this database with customer PII?" No owner tag. No RACI. Audit finding. Tag governance prevents this.
Company D: That was us. The $180K mystery resource. Tag governance now prevents deployments without cost center attribution.
The ROI Math
Cost of tag governance:
- 2 weeks of my time to design policies
- 2 weeks to deploy and remediate
- 1 hour/month to maintain
Total: ~80 hours initial, 12 hours/year maintenance
Savings:
- 6 hours/month of manual reporting (me)
- 4 hours/month of cost reconciliation (finance)
- 2 hours/month of allocation arguments (managers)
- Prevented: $180K in unknown resource waste
Total: 144 hours/year saved + $180K one-time savings
ROI: Obvious.
Deployment Checklist
Week 1: Audit
- [ ] Run tag variation query
- [ ] Document current tag chaos
- [ ] Present findings to leadership
- [ ] Get approval for tag governance project
Week 2: Design
- [ ] Define required tags (Tier 1)
- [ ] Define recommended tags (Tier 2)
- [ ] Create approved enums for each tag
- [ ] Write tag standards document
Week 3: Policy Creation
- [ ] Write Azure Policy definitions
- [ ] Deploy in AUDIT mode only
- [ ] Monitor compliance dashboard
- [ ] Identify edge cases
Week 4: Communication
- [ ] Email organization about new tag standards
- [ ] Provide developer quick-start guide
- [ ] Set remediation deadline (2 weeks)
- [ ] Offer office hours for questions
Week 5: Remediation
- [ ] Run cleanup script on existing resources
- [ ] Work with teams to fix non-compliant resources
- [ ] Document exceptions (if any)
- [ ] Verify compliance at 95%+
Week 6: Enforcement
- [ ] Switch policies from AUDIT to DENY
- [ ] Monitor for blocked deployments
- [ ] Provide support for developers
- [ ] Document edge cases for policy updates
Week 7: Victory
- [ ] Generate first automated cost report
- [ ] Send to finance and leadership
- [ ] Measure time savings
- [ ] Celebrate
FAQ: The Questions Everyone Asks
Q: What if a developer needs to deploy urgently?
A: The policy forces them to add tags. Takes 30 seconds. If it's truly urgent, they can use a valid cost center (even if it's wrong) and fix it later. Better than no tag.
Q: What about legacy resources that can't be tagged?
A: Azure Policy can exclude specific resource types or resource groups. Create an "Exceptions" resource group for truly untaggable resources.
Q: What if we don't have 8-digit cost centers?
A: Adjust the regex. The pattern is ^[0-9]{8}$. Change {8} to your cost center length. The principle stays the same: enforce format consistency.
Q: Can we have more than 4 environments?
A: Yes, but think carefully. Every environment multiplies your complexity. Production/Development/QA/Sandbox covers 99% of cases. If you add "Staging" and "UAT" and "Integration", that's 7 environments. Do you NEED 7?
Q: What if someone puts the wrong cost center?
A: Tag governance enforces FORMAT, not ACCURACY. You can't prevent "78002566" when they meant "78002567". That's a business process problem, not a technical one. But at least the format is consistent for reporting.
Q: How do we handle shared resources?
A: Pick ONE owner. Usually Infrastructure or Operations. Use the "Application" tag to track which applications use it. One resource, one cost center, one owner. Sharing gets allocated in your chargeback model, not your tags.
Q: What about resources created by automation?
A: Your ARM templates, Terraform configs, or Azure DevOps pipelines need to include tags. Add them to the template. Same rules apply to humans and automation.
Next Steps
- Audit your tags - Run the KQL query, see your tag chaos
- Download the starter kit - Get the policies and scripts
- Customize for your org - Adjust cost center format, department names
- Deploy in audit mode - Don't enforce yet, just observe
- Remediate existing resources - Clean up the mess
- Enforce going forward - Switch to deny mode
- Generate cost reports - Show finance the value
Tag governance isn't glamorous.
But it's the difference between:
- "We spent $2.3M on Azure... somewhere"
- "Data: $478K, Marketing: $312K, Development: $1.2M"
One of those answers keeps the CFO happy.
The other one gets your cloud program shut down.
But even perfect tags don't guarantee defensibility—if you can't explain your costs on a napkin, tag compliance is just theater. I explore this deeper in You Can't Govern What You Can't Explain on a Napkin, where defensibility requires more than just accurate data.
Choose wisely.
Want help implementing tag governance at your organization? The Tag Governance Starter Kit includes everything you need: policies, scripts, documentation, and communication templates.
Download it here: Tag Governance Starter Kit (Coming soon)
Or read more about Azure Resource Tagging Best Practices for the technical deep-dive.
Tags saved our Azure program. They can save yours too.
Azure Admin Starter Kit (Free Download)
Get my KQL cheat sheet, 50 Windows + 50 Linux commands, and an Azure RACI template in one free bundle.
Get the Starter Kit →💰 Stop Guessing at Azure Costs
Get the Azure FinOps Framework with cost allocation templates, tag governance policies, and showback dashboards that finance actually understands.
Download FinOps FrameworkExcel template • KQL queries included • No email required