AzurePowerShellEvent GridAutomation

Azure Auto-Tagging Function: Event-Driven Resource Governance

12 min read

Deploy a PowerShell-based Azure Function that automatically tags resources as they're created across multiple subscriptions. No manual tagging, no missing metadata—just instant, intelligent governance.

Ready to Deploy?

Download the complete deployment package including the PowerShell script and comprehensive README with step-by-step deployment instructions.

Download Azure Auto-Tag Function

Package includes: run.ps1 + README.md (7.9 KB)

What It Does

This Azure Function automatically applies governance-compliant tags to resources the moment they're created—without manual intervention or overwriting existing metadata.

It uses Azure Event Grid to listen for resource creation events across all subscriptions, extracts user and creation metadata from the event payload, and applies tags via both PowerShell Az modules and REST API for maximum reliability.

Real-Time Tagging

Tags resources as they're created

Multi-Subscription

Works across all Azure subscriptions

Non-Destructive

Merges tags without overwriting existing ones

Dual Method

PowerShell modules + REST API fallback

Tags Applied Automatically

CreatedBy

User principal name (UPN) from Azure AD claims

CreatedTime

Resource creation timestamp (MM/dd/yyyy)

CreatedUserName

Display name from user claims

CreatedUserIdentity

Identity type (user, app, managed identity)

CreationTimeClientIpAddress

Client IP address from event data

AutoTagged

Flag indicating automatic tagging (True)

How It Works

1

Event Trigger

Azure Event Grid detects a new resource creation event and sends it to the Function App

2

Event Validation

Function validates the event is a user-initiated resource operation (not system/deployment events)

3

Extract Metadata

Extracts user information, timestamp, and IP address from event claims and payload

4

Authenticate

Uses Managed Identity to authenticate with Azure Resource Manager (multiple methods for reliability)

5

Apply Tags

Merges new tags with existing tags and applies them to the resource via PowerShell or REST API

Requirements

Azure Resources

  • Azure Function App (PowerShell 7 runtime)
  • Event Grid System Topic subscription
  • System-assigned Managed Identity enabled

Permissions

  • Tag Contributor role on subscriptions
  • Reader role for resource access
  • 15+ minutes for Az modules to install on new Function Apps

Complete PowerShell Function Script

Below is the complete PowerShell script (run.ps1) that powers the Azure Function. This script includes intelligent event filtering, dual authentication methods, comprehensive error handling, and detailed diagnostics logging.

param(
    [Parameter(Mandatory=$false)]
    [object]$TriggerMetadata,
    [Parameter(Mandatory=$false)]
    [object]$EventGridEvent
)

try {
    Write-Host "---------------------PowerShell EventGrid trigger function started processing event.--------------------------------"

    # Log the raw event (Optional for debugging)
    Write-Host "****Event data:**"
    $data = $EventGridEvent | ConvertTo-Json
    Write-Host "****Meta Data: $($data)"
    Write-Host "****Event data End:**"

    # IMPORTANT: Filter events to only process resource creation/modification by users
    $eventType = $EventGridEvent.eventType
    $operationName = $EventGridEvent.data.operationName
    $userIdentity = $EventGridEvent.data.claims.idtyp

    Write-Host "Event Type: $eventType"
    Write-Host "Operation: $operationName"
    Write-Host "Identity Type: $userIdentity"

    # Skip events that are not user-initiated resource operations
    $skipReasons = @()

    if ($userIdentity -eq "app") {
        $skipReasons += "System/App identity (not user)"
    }

    if ($eventType -notmatch "ResourceWrite|ResourceAction" -or $eventType -match "ResourceDelete") {
        $skipReasons += "Not a resource write operation"
    }

    if ($operationName -match "listKeys|secrets|sync|restart|start|stop") {
        $skipReasons += "Management operation (not resource creation/modification)"
    }

    # Skip deployment-related events (these are Azure Resource Manager operations)
    if ($EventGridEvent.data.resourceUri -match "/deployments/") {
        $skipReasons += "ARM deployment event (not actual resource)"
    }

    if ($skipReasons.Count -gt 0) {
        Write-Host "SKIPPING EVENT - Reasons: $($skipReasons -join ', ')"
        Write-Host "This is normal - only user-created resources should be auto-tagged."
        return
    }

    Write-Host "EVENT ACCEPTED - This appears to be a user-initiated resource operation"

    # Get the resource ID from the event
    $ResourceId = $EventGridEvent.data.resourceUri
    Write-Host "****Resource ID: $($ResourceId)****"

    $UserName = $EventGridEvent.data.claims.name
    Write-Host "****Name: $($UserName)****"

    $UserIdentity = $EventGridEvent.data.claims.idtyp
    Write-Host "****Identity: $($UserIdentity)****"

    $nameClaim = $EventGridEvent.data.claims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name']
    Write-Host "****NameClaim: $($nameClaim)****"

    $EventTime = $EventGridEvent.eventTime
    Write-Host "****EventTime: $($EventTime)****"

    $ClientIpAddress = $EventGridEvent.data.claims.ipaddr
    Write-Host "****ClientIpAddr: $($ClientIpAddress)****"

    # Get Resource details
    $createdby = $nameClaim
    Write-Host "****Resource Created By: $($createdby)****"

    $creationtime = ($EventTime).ToString("MM/dd/yyyy")
    Write-Host "****Resource Creation Time: $($creationtime)****"

    $Name = $UserName
    Write-Host "****Resource Created User Name: $($UserName)****"

    $IdentityType = $UserIdentity
    Write-Host "****Resource Created User Identity: $($UserIdentity)****"

    $CreationTimeClientIpAdress = $ClientIpAddress
    Write-Host "****Resource Created User IpAddress: $($ClientIpAddress)****"

    # Validation: Skip if we don't have user information
    if ([string]::IsNullOrEmpty($createdby) -and [string]::IsNullOrEmpty($UserName)) {
        Write-Host "SKIPPING - No user information available for tagging"
        return
    }

    # Tags to be applied
    $finalTags = @{
        'CreatedBy' = if($createdby) { $createdby } else { "Unknown" }
        'CreatedTime' = ($creationtime)
        'CreatedUserName' = if($UserName) { $UserName } else { "Unknown" }
        'CreatedUserIdentity' = ($UserIdentity)
        'CreationTimeClientIpAdress' = if($ClientIpAddress) { $ClientIpAddress } else { "Unknown" }
        'AutoTagged' = 'True'
    }

    Write-Host "****Final Tag Details: $($finalTags | ConvertTo-Json -Depth 2)****"

    # DIAGNOSTICS: Check Function App environment
    Write-Host "=== DIAGNOSTICS START ==="
    Write-Host "PowerShell Version: $($PSVersionTable.PSVersion)"
    Write-Host "Function App Name: $($env:WEBSITE_SITE_NAME)"
    Write-Host "Az Modules Available:"
    $azModules = Get-Module -ListAvailable Az* | Select-Object Name, Version
    if ($azModules) {
        $azModules | ForEach-Object { Write-Host "  $($_.Name) v$($_.Version)" }
    } else {
        Write-Host "  No Az modules found"
    }

    Write-Host "Authentication Environment Variables:"
    Write-Host "  MSI_ENDPOINT: $($env:MSI_ENDPOINT)"
    Write-Host "  IDENTITY_ENDPOINT: $($env:IDENTITY_ENDPOINT)"
    Write-Host "  IDENTITY_HEADER: $($env:IDENTITY_HEADER -ne $null)"
    Write-Host "=== DIAGNOSTICS END ==="

    # Initialize tagging success flag
    $taggingSuccess = $false

    # Method 1: Try Az PowerShell modules first
    try {
        Write-Host "Attempting PowerShell method..."

        # Check if Az modules are available
        if (Get-Module -ListAvailable Az.Resources -ErrorAction SilentlyContinue) {
            Write-Host "Az modules found, importing..."
            Import-Module Az.Accounts -Force
            Import-Module Az.Resources -Force

            # Try different authentication methods
            $context = $null

            # Method 1a: Standard Managed Identity
            try {
                Write-Host "Trying standard Managed Identity..."
                $context = Connect-AzAccount -Identity -ErrorAction Stop
                Write-Host "Standard Managed Identity: SUCCESS"
            } catch {
                Write-Host "Standard Managed Identity failed: $($_.Exception.Message)"
            }

            if ($context) {
                Write-Host "Successfully authenticated: $($context.Context.Account.Id)"

                # Apply tags using PowerShell
                Update-AzTag -ResourceId $ResourceId -Tag $finalTags -Operation Merge -ErrorAction Stop
                Write-Host "PowerShell: Successfully applied tags to resource: $ResourceId"
                $taggingSuccess = $true
            }

        } else {
            Write-Host "Az modules not available, will try REST API method"
        }
    } catch {
        Write-Host "PowerShell tagging failed: $($_.Exception.Message)"
        Write-Host "Falling back to REST API method..."
    }

    # Method 2: Enhanced REST API with multiple token endpoints
    if (-not $taggingSuccess) {
        try {
            Write-Host "Attempting REST API method..."

            $accessToken = $null

            # Try multiple token endpoint approaches
            $tokenMethods = @()

            # Method 2a: Standard IMDS endpoint
            $tokenMethods += @{
                Name = "Standard IMDS"
                Uri = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
                Headers = @{ Metadata = "true" }
            }

            # Method 2b: Function App specific endpoint (if available)
            if ($env:IDENTITY_ENDPOINT -and $env:IDENTITY_HEADER) {
                $tokenMethods += @{
                    Name = "Function App Identity Endpoint"
                    Uri = "$($env:IDENTITY_ENDPOINT)?resource=https://management.azure.com/&api-version=2019-08-01"
                    Headers = @{ 'X-IDENTITY-HEADER' = $env:IDENTITY_HEADER }
                }
            }

            # Method 2c: MSI endpoint (if available)
            if ($env:MSI_ENDPOINT -and $env:MSI_SECRET) {
                $tokenMethods += @{
                    Name = "MSI Endpoint"
                    Uri = "$($env:MSI_ENDPOINT)?resource=https://management.azure.com/&api-version=2017-09-01"
                    Headers = @{ 'Secret' = $env:MSI_SECRET }
                }
            }

            # Try each token method
            foreach ($method in $tokenMethods) {
                try {
                    Write-Host "Trying token method: $($method.Name)"
                    Write-Host "Token URI: $($method.Uri)"

                    $tokenResponse = Invoke-RestMethod -Uri $method.Uri -Headers $method.Headers -Method Get -TimeoutSec 30
                    $accessToken = $tokenResponse.access_token
                    Write-Host "$($method.Name): Successfully obtained access token"
                    break
                } catch {
                    Write-Host "$($method.Name) failed: $($_.Exception.Message)"
                    continue
                }
            }

            if (-not $accessToken) {
                throw "All token acquisition methods failed"
            }

            # Get current tags first (to merge, not replace)
            $getCurrentTagsUri = "https://management.azure.com$ResourceId/providers/Microsoft.Resources/tags/default?api-version=2021-04-01"
            $getCurrentTagsHeaders = @{
                Authorization = "Bearer $accessToken"
                'Content-Type' = 'application/json'
            }

            $currentTagsResponse = $null
            try {
                $currentTagsResponse = Invoke-RestMethod -Uri $getCurrentTagsUri -Headers $getCurrentTagsHeaders -Method Get
                Write-Host "REST API: Retrieved current tags"
            } catch {
                Write-Host "REST API: No existing tags found (normal for new resources)"
            }

            # Merge current tags with new tags
            $allTags = @{}
            if ($currentTagsResponse -and $currentTagsResponse.properties -and $currentTagsResponse.properties.tags) {
                $currentTagsResponse.properties.tags.PSObject.Properties | ForEach-Object {
                    $allTags[$_.Name] = $_.Value
                }
                Write-Host "REST API: Found $($allTags.Count) existing tags"
            }

            # Add/update with new tags
            $finalTags.GetEnumerator() | ForEach-Object {
                $allTags[$_.Key] = $_.Value
            }

            # Apply tags using REST API
            $updateTagsUri = "https://management.azure.com$ResourceId/providers/Microsoft.Resources/tags/default?api-version=2021-04-01"
            $updateTagsHeaders = @{
                Authorization = "Bearer $accessToken"
                'Content-Type' = 'application/json'
            }

            $tagsPayload = @{
                operation = "Replace"
                properties = @{
                    tags = $allTags
                }
            } | ConvertTo-Json -Depth 10

            Write-Host "Applying tags with payload: $tagsPayload"
            $updateResponse = Invoke-RestMethod -Uri $updateTagsUri -Headers $updateTagsHeaders -Method Patch -Body $tagsPayload
            Write-Host "REST API: Successfully applied tags to resource: $ResourceId"
            Write-Host "REST API: Applied $($allTags.Count) total tags"
            $taggingSuccess = $true

        } catch {
            Write-Host "REST API tagging failed: $($_.Exception.Message)"
            Write-Host "Full REST API error: $($_ | Out-String)"
        }
    }

    # Final result
    if ($taggingSuccess) {
        Write-Host "SUCCESS: Tags applied successfully to resource: $ResourceId"
    } else {
        Write-Host "ERROR: Failed to apply tags using both PowerShell and REST API methods"
        Write-Host "TROUBLESHOOTING INFO:"
        Write-Host "1. Check if Managed Identity is enabled in Function App > Identity"
        Write-Host "2. Verify RBAC permissions (Tag Contributor or Contributor role)"
        Write-Host "3. Allow 15+ minutes for Az modules to install on new Function Apps"
        Write-Host "4. Verify the Function App can access Azure Instance Metadata Service"
    }

} catch {
    Write-Host "Error processing event. $($_.Exception.Message)"
    Write-Host "Full error details: $($_ | Out-String)"
}

Why Use This Solution?

Business Value

  • • Eliminate manual tagging overhead
  • • Ensure 100% tag compliance across resources
  • • Enable accurate cost allocation and chargeback
  • • Improve security and audit capabilities
  • • Reduce governance exceptions and drift

Technical Benefits

  • • Event-driven, serverless architecture
  • • Dual authentication (PowerShell + REST API)
  • • Non-destructive tag merging
  • • Multi-subscription support
  • • Comprehensive error handling and logging