Posted in:

This is the third part in a series about how we can build a serverless workflow using Azure Durable Functions, but implement some of the activities in that workflow using containers with Azure Container Instances. Today we'll look at creating an Event Grid subscription and an Azure Function triggered by an event.

Current table of contents:

Azure Event Grid is a service that acts as a centralized hub that can route events raised by any Azure services (or your own custom events) to any number of subscribers. Many Azure services already publish events to Event Grid, so if you want to know when a deployment completes or when a new blob is uploaded to a storage container, you can use Event Grid to be notified. It uses a push model rather than polling like you'd need to with Service Bus, so Event Grid will call you when an interesting event happens (and can retry with back-off if necessary).

Creating an Event Grid Triggered Azure Function

To support Event Grid triggered functions in Azure Functions, we need to reference the Microsoft.Azure.WebJobs.Extensions.EventGrid NuGet package. Simply add this line to your .csproj file.

<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.EventGrid" 
    Version="2.0.0" />

Then creating an Event Grid triggered function is really easy. You just need to apply the EventGridTrigger attribute to an EventGridEvent parameter and it, and it will be called whenever an event fires. In this example, I'm simply logging out interesting information about the events received. In my case I'm particularly interested in events with the Microsoft.ContainerInstance resource provider. Subscriptions can be filtered so you only receive event types you are interested in.

[FunctionName("AciMonitor")]
public static void Run([EventGridTrigger]EventGridEvent eventGridEvent, 
                       ILogger log)
{
    log.LogInformation($"EVENT: {eventGridEvent.EventType}-{eventGridEvent.Subject}-{eventGridEvent.Topic}");
    log.LogInformation(eventGridEvent.Data.ToString());
    
    // some example properties on data:
    // "resourceProvider": "Microsoft.ContainerInstance"
    // "status": "Succeeded"
    // "resourceUri": "/subscriptions/my-sub-id/resourceGroups/DurableFunctionsAciContainers/providers/Microsoft.ContainerInstance/containerGroups/markacitest1",
    dynamic data = eventGridEvent.Data;
    if (data.operationName == "Microsoft.ContainerInstance/containerGroups/delete")
    {
        log.LogInformation($"Deleted container group {data.resourceUri} with status {data.status}");
    }
    else if (data.operationName == "Microsoft.ContainerInstance/containerGroups/write")
    {
        log.LogInformation($"Created or updated container group {data.resourceUri} with status {data.status}");
    }
}

Generate the endpoint address

Now you might be wondering how we connect up an Event Grid subscription to this function. Well, we simply need to create an Event Grid subscription, specifying what we are interested in receiving, and giving it the URL of this function. Where it gets complicated is that the URL needs to include a secret code that is astonishingly hard to automate the retrieval of. Hopefully this is something the Azure Functions team will improve in the future (it does seem like some work is being done in this area)

We need to get several different keys, so we'll use some PowerShell functions I discuss in this article

First a function that can get the Kudu credentials for a Function App

function getKuduCreds($appName, $resourceGroup)
{
    $user = az webapp deployment list-publishing-profiles `
            -n $appName -g $resourceGroup `
            --query "[?publishMethod=='MSDeploy'].userName" -o tsv

    $pass = az webapp deployment list-publishing-profiles `
            -n $appName -g $resourceGroup `
            --query "[?publishMethod=='MSDeploy'].userPWD" -o tsv

    $pair = "$($user):$($pass)"
    $bytes = [System.Text.Encoding]::ASCII.GetBytes($pair)
    $encodedCreds = [System.Convert]::ToBase64String($bytes)
    return $encodedCreds
}

Second, we need a function that can get the "master" function key given the Kudu creds

function getMasterFunctionKey([string]$appName, [string]$encodedCreds)
{
    $jwt = Invoke-RestMethod `
            -Uri "https://$appName.scm.azurewebsites.net/api/functions/admin/token" `
            -Headers @{Authorization=("Basic {0}" -f $encodedCreds)} -Method GET

    $keys = Invoke-RestMethod -Method GET `
            -Headers @{Authorization=("Bearer {0}" -f $jwt)} `
            -Uri "https://$appName.azurewebsites.net/admin/host/systemkeys/_master" 

    # n.b. Key Management API documentation currently doesn't explain how to get master key correctly
    # https://github.com/Azure/azure-functions-host/wiki/Key-management-API
    # https://$appName.azurewebsites.net/admin/host/keys/_master = does NOT return master key
    # https://$appName.azurewebsites.net/admin/host/systemkeys/_master = does return master key

    return $keys.value
}

Now let's use these functions to get the master key for our Function App:

$kuduCreds = getKuduCreds $functionAppName $resourceGroup
$masterKey = getMasterFunctionKey $functionAppName $kuduCreds

But we're still not done! Now we need to get yet another key - this one is the Event Grid "extension key". We can use the master key we just retrieved to get the extension key:

$extensionKeyUri = "https://$functionAppName.azurewebsites.net/admin/host/systemkeys/eventgrid_extension?code=$masterKey"
$extensionKey = (Invoke-RestMethod -Method GET -Uri $extensionKeyUri).value

Next, we generate the URL of the webhook that Event Grid will use to trigger our Azure Function. You can read about the format of that URL here, but basically we need the name of our function ("AciMonitor" in our case) and the extension key that we just retrieved:

$functionName = "AciMonitor"
$functionUrl = "https://$hostName/runtime/webhooks/EventGrid?functionName=$functionName" + "&code=$extensionKey" 

This function URL is the one that Event Grid will use to report events, but it also uses it to confirm the subscription. That's needed because Event Grid is a push model - it calls your API, and so it needs to check that the endpoint it is calling really does want to receive events. The Azure Functions Event Grid extension already knows how to respond to the validation handshake so you don't need to do anything additional on the Function App side.

However, there is one more pitfall here, and I think it's to do with using the Azure CLI from PowerShell. And that is that we need to escape the & character in the URL to successfully pass it to the Azure CLI command that creates the Event Grid subscription. We do that like this:

$functionUrlEscaped = $functionUrl.Replace("&", "^^^&")

Create the Event Grid Subscription

Finally, we're ready to actually create our subscription. We need to ensure we've registered the Microsoft.EventGrid provider for this subscription, which we can do with the following command:

az provider register -n "Microsoft.EventGrid" 

Now we just need to get hold of the subscription id and the resource id of the resource group we are monitoring for events, and we can create an Event Grid subscription with az eventgrid event-subscription create method, passing in the (escaped) endpoint URL we just constructed. I've asked for all event types in this example, as I want to explore what events are available, but you can filter it down to just the event types you're interested in if you want

$subscriptionId = az account show --query id -o tsv
$resourceId = "/subscriptions/$subscriptionId/resourcegroups/$aciResourceGroup"
az eventgrid event-subscription create --name "AciEvents" `
    --source-resource-id $resourceId `
    --endpoint-type "WebHook" --included-event-types "All" `
    --endpoint $functionUrlEscaped

Note that for this command to execute successfully our Azure Function App needs to be running so it can respond to the validation handshake.

By the way, the Event Grid subscription does not live in a resource group, so if you want to delete it, then you can use the following command:

az eventgrid event-subscription delete --name "AciEvents" `
    --source-resource-id $resourceId

Summary

In this post we saw how we can create an Event Grid subscription that sends events about a particular resource group to an Azure Function. There were a lot of hoops to jump through to get the URL we needed to subscribe - I'm hoping that becomes much easier to automate in the future. But next up in part 4, let's learn how we can use the fluent Azure C# SDK to create ACI container groups from inside our Azure Function App.

Want to learn more about the Azure CLI? Be sure to check out my Pluralsight course Azure CLI: Getting Started.

Comments

Comment by James Carr

Hey,
Great article. Can this method work if I wanted to connect to a different Azure Subscription and deploy this in my own subscription?

James Carr
Comment by Mark Heath

Good question, it's not something I've tried I'm afraid.

Mark Heath
Comment by James Carr

Hey Mark,
Thank you so much for getting back to me. Basically I am looking for a way to pull an Azure subscriptions resource information (VM's, VNET's Storage Account etc.) and place them into a Azure SQL Database. Azure has so many different solutions but cant find one that does this. The data will need to be informational, as in configuration and not event based.
The search will continue.
Have a great day.

James Carr
Comment by Mark Heath

If you're just gathering this information for a whole subscription, you could use Azure PowerShell or Azure CLI to get the info. Check my most recent post showing using PowerShell Azure Functions with privileges to talk to certain resources

Mark Heath
Comment by James Carr

Awesome. Thank you so much, I will check it out. Maybe I am just over complicating things.

James Carr