Using Azure Automation to apply tags to resources

If you’ve ever inherited someone else’s Azure subscription, or just forgot when or why you created that 300th storage account, you can appreciate why it’s important to tag your Azure resources. If you don’t already know, tags are arbitrary name/value pairs of metadata that can be attached to nearly any resource in Azure, which allows you to annotate or classify them in ways that may be difficult or cumbersome to do with resource groups or naming conventions.

As a matter of practice, I try to make a point to add the following tags to all of the resources that I create.

Tag NamePurpose
CreatedThe date (yyyy-mm-dd) that the resource or resource group was created.
DescriptionA brief note about the intended purpose of the resource or resource group.
OwnerMy name or email address, so that it’s immediately obvious to someone else who should be contacted if they have questions about the resource or resource group.

I did say I try to make a point to add these tags. The reality is, sometimes I either forget or the resource was created implicitly (i.e. a Logic Apps API Connection or Network Watcher) and it may slip through the cracks. Azure Policy can be used to enforce these tags, but we can do better — at least for two of them.

While we can’t really automatically apply a Description tag, it should certainly be pretty simple to apply the Owner and Created tags, right? As with just about everything in Azure, there’s more than one way to skin this cat. In this post, I am going to use an Azure Automation runbook in combination with Azure Event Grid to ensure that every Resource Group that gets created in my subscription gets the Owner and Created tags automatically applied.

Azure Event Grid

The Azure Event Grid service, generally speaking, is a basic publish/subscribe messaging service that allows applications to send and receive arbitrary data payloads through a variety of supported endpoints, usually in response to some kind of application-defined activity. You can create your own Event Grid Topics for managing your application’s custom events, but various resources in Azure already have native Event Grid capabilities that handle the publish side automatically. In fact, the Azure subscription itself happens to be one of these resources. In other words, an Azure subscription has built in support for notifying handlers whenever certain events take place, such as ResourceWriteSuccess, ResourceActionSuccess, ResourceDeleteSuccess, failures, etc.

In the previous paragraph, I tried to avoid conflating the terminology of an Azure Subscription (the thing you pay for; the billing unit) and an Event Subscription (a defined link between a published event and an endpoint that handles those events). In this context, the word subscription is used for two entirely different concepts. I’m sure you realize this, but it felt weird writing it, so I wanted to point that out.

Azure Event Grid Subscriptions

Of the various events that the subscription resource publishes, the only one we really care about in this example is ResourceWriteSuccess. We’ll come back to the Events blade shortly, once we have created a runbook to handle the event.

The event data that is sent to web hook subscribers will look something like the following. I have tried my best to sanitize it so that my information is not exposed, while still accurately representing the data format. You may want to use a service like requestbin.com to preview what these event notifications look like, in case you want to handle other event types or debug your event subscriptions.

[
    {
        "subject": "/subscriptions/8a60199c-6e61-4612-b223-d978728b4dbc/resourceGroups/Test",
        "eventType": "Microsoft.Resources.ResourceWriteSuccess",
        "eventTime": "2020-02-07T06:32:49.2273426Z",
        "id": "eec8cb69-fc33-42f9-8b8e-d8a5c48e8e19",
        "data": {
            "authorization": {
                "scope": "/subscriptions/8a60199c-6e61-4612-b223-d978728b4dbc/resourceGroups/Test",
                "action": "Microsoft.Resources/subscriptions/resourceGroups/write",
                "evidence": {
                    "role": "Subscription Admin"
                }
            },
            "claims": {
                "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname": "Einstein",
                "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname": "Josh",
                "groups": "...",
                "ipaddr": "192.168.0.100",
                "name": "Josh Einstein",
                "http://schemas.microsoft.com/identity/claims/objectidentifier": "...",
                "puid": "...",
                "http://schemas.microsoft.com/identity/claims/scope": "user_impersonation",
                "http://schemas.microsoft.com/identity/claims/tenantid": "...",
                "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "user@domain.com",
                "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn": "user@domain.com",
                "ver": "1.0"
            },
            "correlationId": "0113e1e6-79c0-44f5-a57d-dee387916ba3",
            "httpRequest": {
                "clientRequestId": "9b5e9319-b3b6-4647-91a0-7c35b02bb305",
                "clientIpAddress": "192.168.0.100",
                "method": "PUT",
                "url": "https://management.azure.com/subscriptions/8a60199c-6e61-4612-b223-d978728b4dbc/resourceGroups/Test?api-version=2014-04-01-preview"
            },
            "resourceProvider": "Microsoft.Resources",
            "resourceUri": "/subscriptions/8a60199c-6e61-4612-b223-d978728b4dbc/resourceGroups/Test",
            "operationName": "Microsoft.Resources/subscriptions/resourceGroups/write",
            "status": "Succeeded",
            "subscriptionId": "8a60199c-6e61-4612-b223-d978728b4dbc",
            "tenantId": "f58fe80e-a6f2-4326-add3-285b8f0d77b1"
        },
        "dataVersion": "2",
        "metadataVersion": "1",
        "topic": "/subscriptions/8a60199c-6e61-4612-b223-d978728b4dbc"
    }
]

Azure Automation Runbooks

Azure Automation is a service that is used for the foundation of a few different features that (in my opinion anyway) feel completely out of place, such as Update Management for deploying patches and software updates to cloud virtual machines (like WSUS does in a Windows domain) and Inventory/Change Tracking (keeping track of the installed applications and configuration state). What does that have to do with this article? Absolutely nothing other than the fact that those features are built upon a foundational capability in Azure Automation called Runbooks, which are basically just PowerShell scripts that run in the cloud, similar to Azure Functions. But the reason I’m using Azure Automation instead of Azure Functions is that runbooks make it much easier to run authenticated scripts that have access to the resources in your subscription.

If you don’t already have an Azure Automation account built in your subscription, go ahead and create one. Make sure you let it create a “Run As” account, which will be used to access the subscription resources.

There’s no cost to create an Automation account, but billing is based on the number of minutes your runbooks spend executing. You get 500 minutes of run time free per month, and it’s pretty trivial after that for short-lived runbooks like the one we’re creating. See Azure Automation Pricing for more details.

Once you’ve got the account created, go to the Runbooks blade. You’ll see some examples that were created for you, but you’re going to create a new runbook called On_ResourceGroupWrite (or whatever you want) and choose PowerShell as the runbook type. Click the Create button and in a few seconds you’ll be in the code editor. Paste the following code, then click Save, then click Publish. (Publish simply makes your changes “active” and does not cause the script to execute or list it anywhere.)

param (
  # An object representing the data that was posted to the webhook
  # that executes this runbook.
  [Object]$WebhookData
)

# Errors should terminate the script
$ErrorActionPreference = 'Stop'

# Connect to Azure subscription using the Run As account
$ConnectionName = "AzureRunAsConnection"
try {
    $ServicePrincipalConnection = Get-AutomationConnection -Name $ConnectionName
    Write-Output "Logging in to Azure..."
    Add-AzureRmAccount -ServicePrincipal `
        -TenantId $ServicePrincipalConnection.TenantId `
        -ApplicationId $ServicePrincipalConnection.ApplicationId `
        -CertificateThumbprint $ServicePrincipalConnection.CertificateThumbprint
}
catch {
    if (!$ServicePrincipalConnection) {
        Write-Error -Message "Connection $ConnectionName not found."
    }
    else {
        Write-Error -Message $_.Exception
        throw $_.Exception
    }
}

# The RequestBody property will contain the raw JSON data
# that was posted to the webhook.
if ($WebhookData.RequestBody) {
    
    # Event Grid always posts an array of events to the subscriber,
    # even though typically there is only one event in the array.
    # But whatever, we'll handle arrays.
    $ResourceEvents = ConvertFrom-JSON $WebhookData.RequestBody

    foreach ($ResourceEvent in $ResourceEvents) {

        # Although we're also going to add this filter to the
        # subscription, to reduce unnecessary runbook executions,
        # it's here as well for sanity and clarity.
        if ($ResourceEvent.data.operationName -eq "Microsoft.Resources/subscriptions/resourceGroups/write") {

            # A ResourceGroup was created/modified
            # Add tags for Created and Owner

            # Get the existing tags on the Resource Group
            $ResourceGroup = Get-AzureRmResourceGroup -Id $ResourceEvent.subject
            $Tags = $ResourceGroup.Tags

            # Since there's no way to tell the difference between a
            # Resource Group being created or modified (that I know of)
            # we only take action if one or both of the tags we intend
            # to create do not already exist.
            if (!$Tags.Owner -or !$Tags.Created) {

                # Set the Owner to the name of the user that triggered
                # the event, and Created to the date from the event.
                $Tags['Owner'] = $ResourceEvent.data.claims.name
                $Tags['Created'] = '{0:yyyy-MM-dd}' -f ([DateTime]$ResourceEvent.eventTime)

                Set-AzureRmResourceGroup -Id $ResourceEvent.subject -Tag $Tags

            }

        }

    }

}
else {
    Write-Output "WebhookData parameter value was not provided."
}

The comments in the script should explain what it’s doing. This can also be extended to tag individual resources as well, but I’ll leave that as an exercise to the reader.

Once the above script is saved and published, you will need to create a webhook that Event Grid will call when resource events occur. So go back to the Runbook Overview blade, and click the Add Webhook button on the toolbar.

For the name, just give it the same name you gave the runbook (i.e. On_ResourceGroupWrite), leave the enabled slider on, set the expiration to a date far in the future, then (and this is very important) copy the value in the URL field and make a note of it somewhere safe. It won’t be shown again, and this URL can be called from anywhere by default, so you don’t want it to fall into the wrong hands. Treat it like an API key or password.

There’s nothing to provide in the Parameters and Run Settings box, but you have to click it just to get the Create button to enable, so go ahead and do that.

Now we can go back and set up the Event Grid subscription.

Azure Event Grid Subscription

Go back to the Subscription blade in the Azure Portal, as seen in the screen shot earlier, and click on the Events tab. You now have what you need to set up the event subscription.

Click the “+ Event Subscription” button in the toolbar. The name can be whatever you want, something like “subscription-resource-write”. Change the Event Types selection to include only “Resource Write Success”. Change the Endpoint Type to “Web Hook”, then change the Endpoint to the URL you copied in the previous section.

Although the Event Types filter will pass only ResourceWrite events, this actually will push events for any creation or modification of any resource in the subscription. So we need to add an additional filter on the Filters tab to limit it to Resource Group writes.

In the Advanced Filters tab, add a new filter with a key of “data.operationName”, operator of “string is in”, and value of “Microsoft.Resources/subscriptions/resourceGroups/write”. This will make sure our runbook only gets executed whenever a Resource Group is created or modified, conserving those precious 500 minutes of free runtime.

That’s it. Save the event subscription and go create a new Resource Group to test it out. Events are not immediate and it takes some time to spin up a runbook, so it might take a minute, but eventually, you should be able to refresh the newly created Resource Group and see that the tags have been applied.

Conclusion

As I said in the beginning of the post, there’s almost always more than one way to accomplish something in Azure, and there may very well be a much simpler or recommended way of doing this using policies or templates. But knowing how to connect Azure Event Grid to Azure Automation is a useful technique to learn, because with code, the possibilities are endless.

Some other useful automations using the same technique might include:

  • Automatically adding a delete lock on all Key Vaults
  • Adding newly provisioned virtual machines to an external monitoring system like Nagios or ScienceLogic
  • Creating unified Diagnostic Settings on resources so they report to a common Log Analytics Workspace
  • Using the Owner tag to notify the person who owns a resource whenever a modification is made by someone who does not own it

Go forth and automate.