Azure Tag Governance Reality - Why 247 Variations of 'Environment' Collapse Your Cost Reports
Source: Dev.to
The Tag Governance Problem
Policy: “All resources must have an Environment tag”
Reality: Teams create resources with many variations, e.g.:
Environment: ProductionEnv: Prodenvironment: productionEnviroment: Production(typo)Environment: PRODUCTIONEnv: P
Result: Cost reports show 247 variations. Finance cannot group costs.
Query all “Environment” tags (Kusto)
Resources
| extend envTag = tostring(tags.Environment)
| summarize count() by envTag
| order by count_ desc
Sample results
| envTag | count |
|---|---|
| Production | 847 |
| Prod | 312 |
| PRODUCTION | 156 |
| production | 89 |
| P | 67 |
| PRD | 43 |
| … | … |
| Total | 247 unique values for “Production” alone |
Why the policy fails
- What it checks: Tag key exists.
- What it doesn’t check:
- Value is valid
- Capitalization is consistent
- Spelling is correct
Result: The tag exists, but the value is essentially garbage.
“Environment tag required” – work‑around by teams
Teams often create a resource with Environment: "TODO" just to satisfy the policy, then never fix it. The Azure portal lets users:
- Free‑type tag values
- Ignore suggested values
- Create typos
- Use any capitalization
tags = {
Environment = var.environment # What’s in the variable?
}
If the variable contains "prod", "PROD" or "Production" the Terraform code is syntactically valid, but it defeats governance.
Finance request: “Show me Production costs vs Non‑Production”
Without proper governance the query must enumerate every possible variation:
Resources
| extend env = tostring(tags.Environment)
| where env in ("Production", "Prod", "PRODUCTION", "production",
"PRD", "prd", "P", "p", "Prod1", "Production1" /* … */)
Missing costs: 43 resources tagged PRD instead of Production → $12,000/month unaccounted for.
Enforcing a Controlled Vocabulary
Allowed values for the Environment tag
Production(exactly)StagingDevelopmentSandbox
No abbreviations, no variations, no typos.
Azure Policy (JSON)
{
"mode": "Indexed",
"policyRule": {
"if": {
"anyOf": [
{
"field": "tags['Environment']",
"exists": "false"
},
{
"field": "tags['Environment']",
"notIn": ["Production", "Staging", "Development", "Sandbox"]
}
]
},
"then": {
"effect": "deny"
}
}
}
Result: Invalid values are blocked at creation.
Finding non‑standard values (Kusto)
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 (PowerShell)
# 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
}
Terraform Variable Validation (HCL)
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"
}
}
Required Tag Taxonomy
| Tag | Allowed values / format |
|---|---|
| Environment | Production | Staging | Development | Sandbox |
| CostCenter | 4‑digit code from finance |
| Owner | Email address (validated) |
| Application | Application name from CMDB |
| Project (optional) | Project code |
| Backup (optional) | Daily | Weekly | None |
| Compliance (optional) | PCI | HIPAA | SOX | None |
Key principle: Every tag has defined allowed values. Free‑text is limited to the Owner email.
Implementation Steps
- Deploy deny policies for new resources (no impact on existing ones).
- Run KQL queries to locate non‑compliant resources.
- Bulk remediate using PowerShell or other automation.
- Educate teams on the taxonomy and standards.
- Enable deny mode after existing resources are fixed.
Before & After
| Metric | Before | After |
|---|---|---|
| Unique Environment tag values | 247 | 4 (only valid) |
| Time spent cleaning reports | ~2 hours in Excel | ~30 seconds via Azure Cost Management |
| Finance trust in Azure cost data | Low | High – used for budgeting |
Further Reading
- 👉 [Azure Tag Governance Complete Guide] – full taxonomy, policy templates, remediation scripts, and enforcement timeline.