Azure Auto-Tagging Function: Event-Driven Resource Governance
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 FunctionPackage 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
Event Trigger
Azure Event Grid detects a new resource creation event and sends it to the Function App
Event Validation
Function validates the event is a user-initiated resource operation (not system/deployment events)
Extract Metadata
Extracts user information, timestamp, and IP address from event claims and payload
Authenticate
Uses Managed Identity to authenticate with Azure Resource Manager (multiple methods for reliability)
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