DEV Community

David
David

Posted on • Originally published at azure-noob.com

Azure Tag Governance Reality - Why 247 Variations of "Environment" Collapse Your Cost Reports

The Tag Governance Problem

Policy: "All resources must have Environment tag"

Reality: Teams create resources with:

  • Environment: Production
  • Env: Prod
  • environment: production
  • Enviroment: Production (typo)
  • Environment: PRODUCTION
  • Env: 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
Enter fullscreen mode Exit fullscreen mode

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?
}
Enter fullscreen mode Exit fullscreen mode

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"...)
Enter fullscreen mode Exit fullscreen mode

Missing costs:

  • 43 resources tagged PRD instead of Production
  • $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)
  • Staging
  • Development
  • Sandbox

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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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)