Azure Auto-Tagging Function

A PowerShell-based Azure Function App script designed to automatically tag newly created Azure resources, ensuring consistent governance and cost tracking.

🔍 Problem Overview

In enterprise Azure environments, resources are often created by multiple users, automation systems, or DevOps pipelines. However, tracking who deployed what, when it was deployed, and why it exists becomes increasingly difficult—especially when resources are modified later or deployed outside Infrastructure as Code (IaC) workflows.

Without consistent tagging:

  • Accountability is lost - No way to identify who created resources
  • Cost tracking becomes unreliable - Cannot allocate costs to teams or projects
  • Governance and compliance suffer - Difficult to enforce organizational policies
  • Manual audits become necessary - Time-consuming and error-prone

Manual tagging is inconsistent and often forgotten, while policy-based enforcement can block deployments and frustrate users. An automated, event-driven approach provides the best balance of governance and user experience.

🎯 Solution Objectives

The Azure Auto-Tagging Function is designed to solve these challenges through an event-driven approach that:

Automatically tags resources at creation

No user intervention required

Derives values dynamically

From claims, subscription name, resource group name

Avoids overwriting tags during updates

Preserves existing tag values

Ensures coverage across all Azure resources

Works with any resource emitting ResourceWriteSuccess events

By meeting these objectives, the solution provides a robust foundation for resource governance, cost management, and operational accountability without disrupting user workflows.

🏗️ Solution Architecture

Components

  • Azure Function App

    PowerShell-based function that processes resource creation events and applies tags

  • Event Grid Subscription

    Listens for ResourceWriteSuccess events across the subscription

  • Managed Identity

    Securely authenticates the function to Azure resources without credentials

  • PowerShell Script

    Core logic that processes events and applies appropriate tags

How It Works

  1. A resource is created or updated in Azure
  2. Azure Event Grid captures the ResourceWriteSuccess event
  3. The event is routed to the Azure Function
  4. The PowerShell function extracts resource details from the event
  5. The function determines appropriate tag values based on context
  6. Tags are applied to the resource using the Azure Resource Manager API
  7. The function logs the operation for auditing purposes

Key Benefits of This Architecture

  • Serverless - No infrastructure to manage
  • Event-driven - Real-time response to resource changes
  • Scalable - Handles enterprise-level resource creation
  • Non-blocking - Doesn't interfere with deployments
  • Secure - Uses managed identities instead of credentials

🛠️ Implementation Guide

Prerequisites

  • Azure subscription with Contributor access
  • Azure CLI or PowerShell installed locally
  • Visual Studio Code (recommended for script editing)

Step 1: Create the Function App

# Create a resource group for the function
az group create --name rg-auto-tagging --location eastus

# Create a storage account for the function
az storage account create --name stautotag[unique] \
    --resource-group rg-auto-tagging \
    --location eastus \
    --sku Standard_LRS

# Create the function app with PowerShell runtime
az functionapp create --name func-auto-tagging \
    --resource-group rg-auto-tagging \
    --storage-account stautotag[unique] \
    --consumption-plan-location eastus \
    --runtime powershell \
    --functions-version 4 \
    --os-type Windows

Step 2: Enable System-Assigned Managed Identity

# Enable system-assigned managed identity
az functionapp identity assign \
    --name func-auto-tagging \
    --resource-group rg-auto-tagging

# Assign Contributor role to the function app's managed identity
# Get the principal ID first
principalId=$(az functionapp identity show \
    --name func-auto-tagging \
    --resource-group rg-auto-tagging \
    --query principalId -o tsv)

# Assign role at subscription level
az role assignment create \
    --assignee $principalId \
    --role "Contributor" \
    --scope "/subscriptions/$(az account show --query id -o tsv)"

Step 3: Create the Function

Create a new function in the Azure portal or using Azure Functions Core Tools:

  1. Navigate to your function app in the Azure portal
  2. Click on "Functions" in the left menu
  3. Click "Add" to create a new function
  4. Select "Azure Event Grid trigger"
  5. Name the function "AutoTagResources"
  6. Click "Create"

📜 PowerShell Script

Replace the default function code with the following PowerShell script:

# Input bindings are passed in via param block
param($eventGridEvent, $TriggerMetadata)

# Define tag schema
$tagSchema = @{
    "Creator" = $null
    "CreatedDate" = $null
    "Environment" = $null
    "Project" = $null
    "Department" = $null
    "IDSApplicationOwner-Symphony" = $null
}

# Initialize Azure context
Connect-AzAccount -Identity

# Process the event
try {
    # Extract resource information from the event
    $resourceId = $eventGridEvent.data.resourceUri
    $operationName = $eventGridEvent.data.operationName
    $status = $eventGridEvent.data.status
    $timestamp = $eventGridEvent.eventTime
    $claims = $eventGridEvent.data.claims
    
    # Log event information
    Write-Host "Processing event for resource: $resourceId"
    Write-Host "Operation: $operationName"
    Write-Host "Status: $status"
    
    # Only process resource creation events
    if ($operationName -like "*/write" -and $status -eq "Succeeded") {
        # Extract resource details
        $resourceGroup = ($resourceId -split '/')[4]
        $resourceType = ($resourceId -split '/')[6..7] -join '/'
        $resourceName = ($resourceId -split '/')[-1]
        
        # Get existing resource and its tags
        $resource = Get-AzResource -ResourceId $resourceId
        $existingTags = $resource.Tags
        if ($null -eq $existingTags) {
            $existingTags = @{}
        }
        
        # Prepare new tags
        $newTags = $existingTags.Clone()
        
        # Set Creator tag if not exists
        if (-not $newTags.ContainsKey("Creator")) {
            # Try to get creator from claims
            if ($claims.ContainsKey("name")) {
                $creator = $claims["name"]
            } elseif ($claims.ContainsKey("appid")) {
                $creator = "Service Principal: " + $claims["appid"]
            } else {
                $creator = "Unknown"
            }
            $newTags["Creator"] = $creator
        }
        
        # Set CreatedDate tag if not exists
        if (-not $newTags.ContainsKey("CreatedDate")) {
            $newTags["CreatedDate"] = (Get-Date $timestamp).ToString("yyyy-MM-dd")
        }
        
        # Try to determine Environment from resource group name
        if (-not $newTags.ContainsKey("Environment")) {
            if ($resourceGroup -match "prod|production") {
                $newTags["Environment"] = "Production"
            } elseif ($resourceGroup -match "dev|development") {
                $newTags["Environment"] = "Development"
            } elseif ($resourceGroup -match "test|testing|qa") {
                $newTags["Environment"] = "Test"
            } elseif ($resourceGroup -match "stage|staging") {
                $newTags["Environment"] = "Staging"
            } else {
                $newTags["Environment"] = "Unknown"
            }
        }
        
        # Try to determine Project from resource group name
        if (-not $newTags.ContainsKey("Project")) {
            # Extract project code if resource group follows naming convention
            if ($resourceGroup -match "rg-([a-zA-Z0-9]+)") {
                $newTags["Project"] = $matches[1]
            } else {
                $newTags["Project"] = "Unknown"
            }
        }
        
        # Set Department tag if not exists
        if (-not $newTags.ContainsKey("Department")) {
            # Try to determine from subscription
            $subscription = (Get-AzContext).Subscription.Name
            if ($subscription -match "finance|accounting") {
                $newTags["Department"] = "Finance"
            } elseif ($subscription -match "it|infra") {
                $newTags["Department"] = "IT"
            } elseif ($subscription -match "hr|human") {
                $newTags["Department"] = "HR"
            } elseif ($subscription -match "sales|marketing") {
                $newTags["Department"] = "Sales"
            } else {
                $newTags["Department"] = "Unknown"
            }
        }
        
        # Set IDSApplicationOwner-Symphony tag if not exists
        if (-not $newTags.ContainsKey("IDSApplicationOwner-Symphony")) {
            if ($claims.ContainsKey("name")) {
                $newTags["IDSApplicationOwner-Symphony"] = $claims["name"]
            } else {
                $newTags["IDSApplicationOwner-Symphony"] = "Unknown"
            }
        }
        
        # Apply tags if they've changed
        if (($newTags.Count -gt $existingTags.Count) -or 
            ($newTags.Keys | Where-Object { $newTags[$_] -ne $existingTags[$_] })) {
            
            Write-Host "Applying tags to resource: $resourceName"
            
            # Update resource tags
            Update-AzTag -ResourceId $resourceId -Tag $newTags -Operation Merge
            
            Write-Host "Tags applied successfully"
        } else {
            Write-Host "No new tags to apply"
        }
    } else {
        Write-Host "Skipping event - not a successful resource creation"
    }
} catch {
    Write-Error "Error processing event: $_"
    throw $_
}

Key Script Features

  • Tag Schema Definition - Defines the standard tags to be applied
  • Managed Identity Authentication - Uses system-assigned identity for secure access
  • Intelligent Tag Derivation - Extracts values from context when possible
  • Non-Destructive Updates - Preserves existing tags using merge operation
  • Error Handling - Robust error handling with detailed logging

🏷️ Tag Schema

The solution applies the following standard tags to resources:

Tag NameDescriptionSourceExample
CreatorPerson or system that created the resourceEvent claims"John Doe" or "Service Principal: 12345678-1234-1234-1234-123456789012"
CreatedDateDate when the resource was createdEvent timestamp"2025-06-01"
EnvironmentDeployment environmentResource group name pattern"Production", "Development", "Test", "Staging"
ProjectProject or application identifierResource group name pattern"FinApp", "CRM", "DataLake"
DepartmentOrganizational departmentSubscription name pattern"Finance", "IT", "HR", "Sales"
IDSApplicationOwner-SymphonyApplication owner identifierEvent claims"John Doe"

Customizing the Tag Schema

To customize the tag schema for your organization:

  1. Modify the $tagSchema variable in the PowerShell script
  2. Add or remove tag definitions as needed
  3. Update the tag derivation logic in the script
  4. Test thoroughly before deploying to production

📡 Event Grid Setup

To connect your function to Azure Event Grid:

Step 1: Create an Event Grid Subscription

# Get the function URL and key
functionUrl=$(az functionapp function show \
    --name func-auto-tagging \
    --resource-group rg-auto-tagging \
    --function-name AutoTagResources \
    --query invokeUrlTemplate -o tsv)

functionKey=$(az functionapp function keys list \
    --name func-auto-tagging \
    --resource-group rg-auto-tagging \
    --function-name AutoTagResources \
    --query default -o tsv)

# Create the event grid subscription
az eventgrid event-subscription create \
    --name "resource-auto-tagging" \
    --source-resource-id "/subscriptions/$(az account show --query id -o tsv)" \
    --endpoint $functionUrl \
    --endpoint-type azurefunction \
    --included-event-types Microsoft.Resources.ResourceWriteSuccess \
    --subject-begins-with "/subscriptions/" \
    --advanced-filter data.operationName StringContains "Microsoft.Resources/deployments/write" "Microsoft.Resources/deployments/validate/action"

Step 2: Verify Event Grid Subscription

# List event grid subscriptions
az eventgrid event-subscription list \
    --source-resource-id "/subscriptions/$(az account show --query id -o tsv)" \
    --query "[?name=='resource-auto-tagging']"

Important Note

The Event Grid subscription will trigger the function for all resource creation events in the subscription. For large environments, consider filtering by resource group or resource type to reduce function executions.

🧪 Testing & Validation

Test Plan

To verify the auto-tagging function is working correctly:

  1. Create a test resource
    # Create a test storage account
    az storage account create \
        --name sttest[unique] \
        --resource-group rg-auto-tagging \
        --location eastus \
        --sku Standard_LRS
  2. Check function logs
    # View function logs
    az functionapp log tail \
        --name func-auto-tagging \
        --resource-group rg-auto-tagging
  3. Verify tags were applied
    # Check resource tags
    az resource show \
        --name sttest[unique] \
        --resource-group rg-auto-tagging \
        --resource-type "Microsoft.Storage/storageAccounts" \
        --query tags

Validation Scenarios

Test User-Created Resources

Create resources through the portal to verify user identity is captured

Test Service Principal Resources

Deploy resources via CI/CD to verify service principal tagging

Test Tag Preservation

Update resources with existing tags to verify they're preserved

Test Various Resource Types

Create different resource types to ensure broad compatibility

🔧 Troubleshooting

Common Issues

Function Not Triggering

Symptoms: Resources are created but no tags are applied, no function logs

Possible Causes:

  • Event Grid subscription not properly configured
  • Function app is stopped or in error state
  • Event types don't match what's being generated

Solution: Verify Event Grid subscription, check function app status, review event filtering

Permission Errors

Symptoms: Function triggers but fails with permission errors

Possible Causes:

  • Managed identity not configured correctly
  • Insufficient role assignments
  • Resource locks preventing tag updates

Solution: Verify managed identity, check role assignments, look for resource locks

Script Errors

Symptoms: Function triggers but fails with script errors

Possible Causes:

  • PowerShell module version incompatibilities
  • Syntax errors in the script
  • Unexpected event payload format

Solution: Check function logs for detailed error messages, test script locally

Diagnostic Commands

# Check function app status
az functionapp show --name func-auto-tagging --resource-group rg-auto-tagging --query state

# View detailed function logs
az functionapp log tail --name func-auto-tagging --resource-group rg-auto-tagging

# Check managed identity configuration
az functionapp identity show --name func-auto-tagging --resource-group rg-auto-tagging

# Verify role assignments
principalId=$(az functionapp identity show --name func-auto-tagging --resource-group rg-auto-tagging --query principalId -o tsv)
az role assignment list --assignee $principalId

# Check Event Grid subscription
az eventgrid event-subscription show --name "resource-auto-tagging" --source-resource-id "/subscriptions/$(az account show --query id -o tsv)"

✨ Best Practices

Resource Naming Conventions

Establish consistent naming conventions for resources and resource groups to improve tag value derivation.

  • Resource Groups: rg-[project]-[environment]
  • Subscriptions: [department]-[purpose]
  • Resources: [type]-[project]-[environment]

Function App Configuration

Optimize your function app for reliability and performance.

  • Enable Application Insights for monitoring
  • Configure appropriate timeout settings
  • Set up alerts for function failures
  • Use consumption plan for cost efficiency

Security Considerations

Ensure your auto-tagging solution follows security best practices.

  • Use least-privilege permissions for managed identity
  • Enable diagnostic settings for audit logs
  • Implement IP restrictions for function app
  • Regularly review and rotate keys

Operational Excellence

Maintain and improve your auto-tagging solution over time.

  • Document tag schema and derivation logic
  • Implement version control for script changes
  • Set up regular tag compliance reporting
  • Periodically review and update tag schema

🚀 Advanced Scenarios

Multi-Subscription Deployment

For organizations with multiple subscriptions, consider these approaches:

  • Centralized Function: Deploy a single function app with multiple Event Grid subscriptions
  • Distributed Functions: Deploy separate function apps in each subscription
  • Management Group Level: Configure Event Grid at the management group level

Integration with ServiceNow

For organizations using ServiceNow for change management:

# Add to PowerShell script to create ServiceNow change requests for remediation
if ($requiresRemediation) {
    $snowParams = @{
        Uri = "https://your-instance.service-now.com/api/now/table/change_request"
        Method = "POST"
        Headers = @{
            "Accept" = "application/json"
            "Content-Type" = "application/json"
            "Authorization" = "Basic " + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("username:password"))
        }
        Body = @{
            short_description = "Tag remediation for $resourceName"
            description = "Resource $resourceId requires tag remediation"
            category = "Other"
            priority = "4"
        } | ConvertTo-Json
    }
    
    Invoke-RestMethod @snowParams
}

Tag Compliance Reporting

Implement regular tag compliance reporting to identify resources that need attention:

# Sample PowerShell script for tag compliance reporting
$resources = Get-AzResource
$report = @()

foreach ($resource in $resources) {
    $compliance = @{
        ResourceId = $resource.ResourceId
        ResourceName = $resource.Name
        ResourceType = $resource.ResourceType
        ResourceGroup = $resource.ResourceGroupName
        MissingTags = @()
        IncompleteValues = @()
    }
    
    # Check for required tags
    foreach ($tag in @("Creator", "CreatedDate", "Environment", "Project", "Department", "IDSApplicationOwner-Symphony")) {
        if (-not $resource.Tags -or -not $resource.Tags.ContainsKey($tag)) {
            $compliance.MissingTags += $tag
        } elseif ($resource.Tags[$tag] -eq "Unknown" -or [string]::IsNullOrEmpty($resource.Tags[$tag])) {
            $compliance.IncompleteValues += $tag
        }
    }
    
    if ($compliance.MissingTags.Count -gt 0 -or $compliance.IncompleteValues.Count -gt 0) {
        $report += [PSCustomObject]$compliance
    }
}

$report | Export-Csv -Path "TagComplianceReport.csv" -NoTypeInformation

Cost Allocation Dashboards

Use the tags applied by this solution to create powerful cost allocation dashboards in Azure Cost Management:

  1. Navigate to Azure Cost Management in the Azure portal
  2. Create a new view with grouping by your tags (e.g., Department, Project, Environment)
  3. Save the view and share with stakeholders
  4. Schedule regular exports to CSV or Power BI

Get the Azure Auto-Tagging Function

Ready to implement automatic resource tagging in your Azure environment? Download the Azure Auto-Tagging Function script and start improving your cloud governance today.