Menu

How to create an infrastructure for EventGrid and Webhook event handler?

I recently worked with even-driven solution which used Azure Event Grid to route different events to Webhook endpoint. This blog post shows how to configure and create Azure infrastructure for EventGrid and Webhook based event handler. Configuring Webhook type of event handler (subscription) via Bicep is a pretty straighforward operation, but you need to know a few things which will affect how infrastructure creation should be done.

Overall architecture of sample solution

undefined

Events flow through Event Grid which is responsible for routing different type of events to right event handlers (subscription). Azure Event Grid supports multiple event handlers like Webhooks, Azure Functions, Event Hubs, Service Bus Queue & Topics, Relay Hybrid Connections and Storage Queues.

This blog post concentrates to Webhook type of event handler which is hosted in Azure Function. Don't get confused, it's a Azure Function but from the Event Grid point of view event handler is Webhook.

Webhooks are a popular pattern to deliver notifications between applications and via HTTP endpoints. Applications that make notifications available, allow for other applications to register an HTTP endpoint to which notifications are delivered. 

Source: HTTP 1.1 Web Hooks for Event Delivery - Version 1.0

Webhook vs Azure Function based event handler

There are some differences between Webhook and Azure Function type of event handler:

  • Azure AD based authentication is only supported with Webhooks.
  • You need to configure Webhook handshake validation manually when Webhook is used. If Azure Function type event handler is used you don't need to do anything like this.
  • Handling manually Webhook handshake requires also adjustments to IaC (more later about this)
  • Webhook doesn't support all available event schemas (Event Grid, Cloud Events, Custom).
  • Webhook type of event handler enables more wider backend technology support if you don't want to use Azure Functions.
  • Webhook event handler implemented as a Azure Function requires some extra work from Authorization point of view. Especially how to handle Authorization Key rotation via KeyVault.
  • Event Grid automatically adjusts the rate at which events are delivered to a function triggered by an Event Grid event based on the perceived rate at which the function can process events. This rate match feature averts delivery errors that stem from the inability of a function to process events as the function’s event processing rate can vary over time. Source: Use a function as an event handler for Event Grid events

What you need to know before creating the Azure infrastructure?

Configuration of Webhook type of event handler requires always a handshake before configuration can be finalized. Handshake happends immediately during the configuration which means that you need to deploy application which handles the Webhook handshake before starting the actual configuration. Overall this causes some extra work from infrastructure creation point of view because you need to have a specifc order to execute things.

What is Webhook handshake validation?

Any system that allows registration of and delivery of notifications to arbitrary HTTP endpoints can potentially be abused such that someone maliciously or inadvertently registers the address of a system that does not expect such requests and for which the registering party is not authorized to perform such a registration.

It is important to understand is that the handshake does not aim to establish an authentication or authorization context. It only serves to protect the sender from being told to a push to a destination that is not expecting the traffic.

Source: HTTP 1.1 Web Hooks for Event Delivery - Version 1.0

Like said Webhook handsake will be executed immediately if you configure Webhook based event handler (subscription) to Event Grid via Azure Portal or Azure CLI etc. This practically means that your Webhook application (in this case Azure Function) must be deployed before you can configure the Webhook based event handler (subscription) in Event Grid.

How to handle Webhook handshake?

Handshake validation can be handled like this. You can find more information about validation request from here.

if (HttpMethods.IsOptions(req.Method))
{
    string header = req.Headers["WebHook-Request-Origin"];
    req.HttpContext.Response.Headers.Add("Webhook-Allowed-Origin", header);
    return new OkResult();
}

Deployment order

One solution is to split your infrastructure creation in multiple steps and execute it in a specific order. Webhook application must be deployed before event handler (subscription) is possible to configure in Event Grid. 

eventgrid-high-level.webp

1. Deployment of Azure Core Infrastructure

Deployment of Azure Core Infrastructure creates all necessary Azure Resources but it doesn't configure Webhook based event handler (subscription) to Event Grid. From Azure DevOps point of view this deployment can be executed in own deployment pipeline.

undefined

2. Application deployment

Application deployment pipeline deploys a Webhook endpoint which is hosted in Azure Function. Application must manually handle the Webhook handshake.

undefined

3. Post-deployment of Azure Infrastructure

Lastly, a separate post-deployment pipeline creates the Azure Function type of event handler (subscription) configuration to Event Grid. Because Webhook handshake validation code is already in place configuration is possible to finalize.

Bicep implementation

This module orchestrates the configuration of Webhook type of event handler (subscription). Authorization to Azure Function (Webhook endpoint) is handled with Function Key. Function key of the Azure Function is persisted to KeyVault during the deployment of Azure Function resource. 

I noticed during testing that you can actually link Azure Function Keys also automatically to KeyVault. Read more about this from here.

targetScope = 'subscription'

param location string
param eventsResourceGroupName string

var servicePrefix = 'events'
var rgScope = resourceGroup(eventsResourceGroupName)
var resourceToken = toLower(uniqueString(subscription().id, 'events', location))
var abbreviations = loadJsonContent('assets/abbreviations.json')

var topicName = '${abbreviations.eventGridTopic}${servicePrefix}-${location}-${resourceToken}'

var keyVaultName = '${abbreviations.keyVault}${servicePrefix}-${resourceToken}'
var functionAppName = '${abbreviations.functionsApp}${servicePrefix}-${location}-${resourceToken}'
var functionKeySecretName = 'webhook-function-key'

resource functionApp 'Microsoft.Web/sites@2021-03-01' existing = {
  name: functionAppName
  scope: rgScope
}

resource kv 'Microsoft.KeyVault/vaults@2021-11-01-preview' existing = {
  name: keyVaultName
  scope: rgScope
}

var eventGridSubscriptionName = '${abbreviations.eventGridSubscription}${servicePrefix}-${location}-${resourceToken}'
var webhookFunctionPath = '/api/Function1'

module eventGridWebhookSubscriber 'modules/eventGridWebhookSubscription.bicep' = {
  scope:rgScope
  name: eventGridSubscriptionName
  params: {
    topicName: topicName
    subscriptionName: eventGridSubscriptionName
    eventTimeToLiveInMinutes: 1440
    maxDeliveryAttempts: 30
    maxEventsPerBatch: 1
    preferredBatchSizeInKilobytes: 64
    webhookEndpointUrl: 'https://${functionApp.properties.defaultHostName}/${webhookFunctionPath}'
    functionKey: kv.getSecret(functionKeySecretName)
  }
}

This module configures the new event handler (subscription) to Event Grid

param subscriptionName string
param topicName string
param webhookEndpointUrl string
@secure()
param functionKey string
param eventTimeToLiveInMinutes int
param maxDeliveryAttempts int
param maxEventsPerBatch int
param preferredBatchSizeInKilobytes int

resource subscription 'Microsoft.EventGrid/topics/eventSubscriptions@2022-06-15' = {
    name:'${topicName}/${subscriptionName}'
    properties: {
        filter:{
	        enableAdvancedFilteringOnArrays:true
        }
        labels:[]
        eventDeliverySchema:'CloudEventSchemaV1_0'
        retryPolicy:{
	        eventTimeToLiveInMinutes:eventTimeToLiveInMinutes
            maxDeliveryAttempts:maxDeliveryAttempts
        }
        destination:{
	        endpointType: 'WebHook'
            properties:{
                maxEventsPerBatch:maxEventsPerBatch
                preferredBatchSizeInKilobytes:preferredBatchSizeInKilobytes
                endpointUrl: '${webhookEndpointUrl}?code=${functionKey}'
            }         
        }
    }
}

You can find the complete solution from IaC point of view from Github.

Comments