This guide details how to build a robust, event-driven solution using an Azure Automation Account to automatically tag newly created Virtual Machines with the creator’s identity (CreatorId) and the […]
Solution Components:
vm-tagger-automation-rg.vm-tagger-automation-accountEast US.The Automation Account needs permission to modify resources (i.e., add tags) across your subscription.
vm-tagger-automation-account resource.SubscriptionContributorThis is where our tagging script will live.
vm-tagger-automation-account, navigate to “Runbooks” (under “Process Automation” in the left menu).Tag-New-VMPowerShell5.1Tags a newly created VM with CreatorId and CreationDate.Tag-New-VM runbook will open. Delete any existing placeholder text.<#
.SYNOPSIS
This runbook is triggered by an Event Grid event when a new VM is created.
It tags the VM with the creator's identity and the creation timestamp.
#>
# Define the input parameter that will receive data from the webhook.
param (
# The WebhookData object is automatically passed by Azure Automation when a webhook is invoked.
[Parameter(Mandatory = $true)]
[object] $WebhookData
)
# 1. Process the incoming webhook data from Event Grid
# The webhook data contains a JSON string in its RequestBody. We need to parse it.
Write-Output "Webhook received. Processing data..."
# Convert the JSON string from the webhook's body into a PowerShell object for easy access.
$event = $WebhookData.RequestBody | ConvertFrom-Json
# The actual event details are nested within the 'data' property of the main event object.
$data = $event.data
# 2. Log details for debugging purposes. This output will appear in the Automation Job logs.
Write-Output "Event Type: $($event.eventType)"
Write-Output "Resource URI: $($data.resourceUri)"
# 3. Filter to ensure this is the correct event for a VM creation.
# We only want to act on successful resource creation events for virtual machines.
if ($event.eventType -eq "Microsoft.Resources.ResourceWriteSuccess" -and $data.resourceUri -like "*Microsoft.Compute/virtualMachines*") {
Write-Output "VM creation event detected. Proceeding with tagging."
# 4. Extract creator's identity from the event claims.
# The 'upn' (User Principal Name) is the most reliable identifier (e.g., user@domain.com).
$creator = $data.claims.upn
# If the UPN is not available, fall back to the 'name' claim.
if ([string]::IsNullOrWhiteSpace($creator)) {
$creator = $data.claims.name
}
# If no identity can be found, set a default value.
if ([string]::IsNullOrWhiteSpace($creator)) {
$creator = "unknown"
}
# Get the exact time the event occurred.
$creationDate = $event.eventTime
# Log the extracted information for verification.
Write-Output "Creator identified as: $creator"
Write-Output "Creation timestamp: $creationDate"
# 5. Connect to Azure using the Automation Account's System-Assigned Managed Identity.
# This is a secure way to authenticate without storing credentials in the script.
try {
Write-Output "Connecting to Azure with Managed Identity..."
# What this line does:
# 'Connect-AzAccount' is the command to log into Azure.
# The '-Identity' switch tells PowerShell to use the Azure Managed Identity of the
# environment where the script is running. In this case, it's the System-Assigned
# Managed Identity of our Automation Account, which we enabled in Step 1.1.
#
# Why it's important:
# This is the most secure method for authentication in Azure Automation. It avoids
# storing any passwords, secrets, or certificates directly in the script. Azure
# handles the authentication token lifecycle automatically in the background. The
# permissions for this identity were granted in Step 1.2.
#
# '$null = ...'
# The 'Connect-AzAccount' command outputs a summary of the logged-in context
# (subscription, tenant, etc.). By assigning the output to '$null', we are
# suppressing this information from appearing in the job logs, keeping them clean.
$null = Connect-AzAccount -Identity
Write-Output "Successfully connected to Azure."
}
catch {
# If the connection fails, write an error and stop the script.
Write-Error "Failed to connect to Azure using Managed Identity. Please check permissions."
throw
}
# 6. Get the full Azure resource object for the VM that was just created.
# The resource URI is provided in the event data.
$vm = Get-AzResource -ResourceId $data.resourceUri
# 7. Prepare the new tags to be applied.
# First, retrieve any existing tags on the VM to avoid overwriting them.
$tags = $vm.Tags
# If the VM has no existing tags, initialize an empty hashtable.
if ($null -eq $tags) {
$tags = @{}
}
# Add or update the 'CreatorId' tag.
$tags["CreatorId"] = $creator
# Add or update the 'CreationDate' tag.
$tags["CreationDate"] = $creationDate
# 8. Apply the new and updated tags to the VM.
try {
Write-Output "Applying tags to VM..."
# Use Set-AzResource to update the tags on the specified VM.
# The -Force parameter suppresses confirmation prompts.
$vm | Set-AzResource -Tag $tags -Force
Write-Output "Successfully tagged VM: $($vm.Name)"
}
catch {
# If tagging fails, write an error and stop the script.
Write-Error "Failed to apply tags to the VM. Error: $_"
throw
}
}
else {
# If the event is not for a VM creation, log it and do nothing.
Write-Output "Event is not a VM creation event. Ignoring."
}
Tag-New-VM runbook, click on “Webhooks” in the left menu.VM-Creation-WebhookYesWebhookData. In the text box, enter a simple placeholder JSON object:
{}
This step connects the subscription-wide events to your webhook.
Subscriptionsvm-tagger-automation-rg.Subscription-VM-Events-Topic.vm-creation-trigger-for-automation.Resource Write Success.Webhook.data.operationName.String is exactly.Microsoft.Compute/virtualMachines/write.Your automation is now live.
CreatorId: The Azure login of the user who created the VM (e.g., shaleen-wonder).CreationDate: The UTC timestamp of the creation event.This solution only tags new VMs. To tag your existing VMs, run this one-time script in Azure Cloud Shell (PowerShell).
Tag-Existing-VMs.ps1 by running code Tag-Existing-VMs.ps1.# --- Configuration ---
# Define a static value for the creator, since we can't know who originally created these VMs.
$BackfillCreatorId = "backfilled-by-script"
# Define a static creation date, representing when the backfill script was run.
$BackfillCreationDate = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss")
# --- Script Logic ---
Write-Host "Connecting to Azure..."
try {
# Authenticate using the identity of the logged-in user in Cloud Shell.
$null = Connect-AzAccount -Identity
} catch {
# If connection fails, show an error and exit.
Write-Error "Failed to connect. Please ensure you are running this in Cloud Shell or are logged in."
return
}
Write-Host "Finding all VMs in the current subscription..."
# Get a list of all virtual machines in the subscription context.
$vmsToTag = Get-AzVM
# Check if any VMs were found.
if (!$vmsToTag) {
Write-Host "No VMs found."
return
}
Write-Host "Found $($vmsToTag.Count) total VMs."
# Loop through each VM in the collection.
foreach ($vm in $vmsToTag) {
# Check if the 'CreatorId' tag already exists to avoid overwriting it.
if ($vm.Tags.ContainsKey("CreatorId")) {
Write-Host "VM '$($vm.Name)' in RG '$($vm.ResourceGroupName)' already has CreatorId tag. Skipping."
}
else {
Write-Host "Applying backfill tags to VM '$($vm.Name)'..."
# Get the existing tags from the VM.
$newTags = $vm.Tags
# Add the backfill creator ID to the tags hashtable.
$newTags["CreatorId"] = $BackfillCreatorId
# Add the backfill creation date to the tags hashtable.
$newTags["CreationDate"] = $BackfillCreationDate
try {
# Apply the updated tags to the VM.
# -AsJob runs the operation in the background, which is good practice for long-running tasks.
# Wait-Job pauses the script until the background job is complete.
$vm | Set-AzVM -Tags $newTags -AsJob | Wait-Job | Out-Null
Write-Host "Successfully tagged VM '$($vm.Name)'."
}
catch {
# If an error occurs during tagging, report it.
Write-Error "Failed to tag VM '$($vm.Name)'. Error: $_"
}
}
}
Write-Host "Backfill script complete."
./Tag-Existing-VMs.ps1