The Tag Governance Problem
Policy: "All resources must have Environment tag"
Reality: Teams create resources with:
Environment: ProductionEnv: Prodenvironment: production-
Enviroment: Production(typo) Environment: PRODUCTIONEnv: P
Result: Cost reports show 247 variations. Finance can't group costs.
Real Example: Our Tag Chaos
Query all "Environment" tags:
Resources
| extend envTag = tostring(tags.Environment)
| summarize count() by envTag
| order by count_ desc
Results:
-
Production- 847 resources -
Prod- 312 resources -
PRODUCTION- 156 resources -
production- 89 resources -
P- 67 resources -
PRD- 43 resources - 241 more variations...
Total: 247 unique values for "Production" alone
Why Tag Governance Fails
Problem #1: Azure Policy Doesn't Validate Values
Policy: "Require Environment tag"
What it checks: Tag key exists
What it doesn't check:
- Value is valid
- Capitalization is consistent
- Spelling is correct
Result: Tag exists but value is garbage
Problem #2: Teams Work Around Policies
Policy: "Environment tag required"
Team: Creates resource with Environment: "TODO" to pass policy
Later: Never fixes it
Problem #3: No Enforcement at Portal
Portal lets you:
- Free-type tag values
- Ignore suggested values
- Create typos
- Use any capitalization
Problem #4: Terraform/ARM Templates Don't Help
tags = {
Environment = var.environment # What's in the variable?
}
If variable contains "prod" or "PROD" or "Production", all valid Terraform. All wrong for governance.
The Cost Impact
Finance request: "Show me Production costs vs Non-Production"
Without governance:
Resources
| extend env = tostring(tags.Environment)
| where env in ("Production", "Prod", "PRODUCTION", "production",
"PRD", "prd", "P", "p", "Prod1", "Production1"...)
Missing costs:
- 43 resources tagged
PRDinstead ofProduction - $12,000/month unaccounted for in reports
Tag Governance That Actually Works
Step 1: Define Standard Values
Environment tag allowed values:
-
Production(only this, exactly) StagingDevelopmentSandbox
That's it. No abbreviations. No variations. No typos.
Step 2: Azure Policy with Value Enforcement
{
"mode": "Indexed",
"policyRule": {
"if": {
"anyOf": [
{
"field": "tags['Environment']",
"exists": "false"
},
{
"field": "tags['Environment']",
"notIn": ["Production", "Staging", "Development", "Sandbox"]
}
]
},
"then": {
"effect": "deny"
}
}
}
Result: Invalid values blocked at creation
Step 3: Fix Existing Resources
// Find resources with non-standard values
Resources
| extend env = tostring(tags.Environment)
| where env !in ("Production", "Staging", "Development", "Sandbox")
| project name, resourceGroup, currentValue = env
| extend suggestedValue = case(
env in~ ("Prod", "PRD", "P", "PRODUCTION", "production"),
"Production",
env in~ ("Stage", "STG", "S", "STAGING"),
"Staging",
env in~ ("Dev", "D", "DEVELOPMENT", "development"),
"Development",
"Sandbox"
)
Remediation script:
# Get resources with wrong tags
$resources = Get-AzResource | Where-Object {
$_.Tags.Environment -notin @("Production", "Staging", "Development", "Sandbox")
}
# Fix them
foreach ($resource in $resources) {
$currentValue = $resource.Tags.Environment
# Map to standard value
$newValue = switch -Regex ($currentValue) {
"^[Pp](rod|RD)?$" { "Production" }
"^[Ss](tage|taging|TG)?$" { "Staging" }
"^[Dd](ev|EV)?$" { "Development" }
default { "Sandbox" }
}
# Update tag
$resource.Tags.Environment = $newValue
Set-AzResource -ResourceId $resource.ResourceId -Tag $resource.Tags -Force
}
Step 4: Terraform Value Validation
variable "environment" {
type = string
description = "Environment name"
validation {
condition = contains([
"Production",
"Staging",
"Development",
"Sandbox"
], var.environment)
error_message = "Environment must be exactly: Production, Staging, Development, or Sandbox"
}
}
The Tag Taxonomy That Works
Required tags for ALL resources:
-
Environment- Production | Staging | Development | Sandbox -
CostCenter- 4-digit code from finance -
Owner- Email address -
Application- App name from CMDB
Optional tags:
-
Project- Project code -
Backup- Daily | Weekly | None -
Compliance- PCI | HIPAA | SOX | None
Key principle: Every tag has DEFINED allowed values. No free text except Owner email.
Enforcement Timeline
Week 1: Policy Deployment
- Deploy deny policies for new resources
- Existing resources not affected
Week 2-4: Remediation
- Run KQL queries to find non-compliant resources
- Bulk fix with PowerShell scripts
- Team meetings to explain standards
Week 5: Full Enforcement
- All resources compliant
- Deny policies block non-standard values
- Cost reports finally accurate
Real Results
Before:
- 247 Environment tag variations
- Cost reports required 2 hours of Excel cleanup
- Finance didn't trust Azure cost data
After:
- 4 Environment tag values (only valid ones)
- Cost reports accurate in 30 seconds
- Finance trusts data, uses it for budgeting
Common Mistakes
❌ Mistake #1: Too Many Tags
Bad: Require 15 tags on every resource
Result: Teams copy-paste garbage to pass policy
Good: 4 required tags that matter
❌ Mistake #2: Free-Text Values
Bad: Allow any value for "Owner" tag
Result: "John", "john.doe", "j.doe@company.com", "IT Team"
Good: Validate email format with policy
❌ Mistake #3: No Remediation Plan
Bad: Deploy deny policy, existing resources broken
Result: Production deploys fail, emergency policy exemptions
Good: Fix existing resources BEFORE enforce mode
Full Governance Framework
Complete tag taxonomy, Azure Policy templates, remediation scripts, and enforcement timeline:
👉 Azure Tag Governance Complete Guide
Implementing tag governance? Define allowed values, enforce with policy, remediate existing resources, then enable deny mode. In that order.
Top comments (0)