How to create an infrastructure for EventGrid and Webhook event handler?
I recently worked with an even-driven solution that used Azure Event Grid to route different events to the Webhook endpoint. This blog post shows how to configure and create Azure infrastructure for EventGrid and Webhook-based event handlers. Configuring a Webhook type of event handler (subscription) via Bicep is a pretty straightforward operation, but you need to know a few things that will affect how infrastructure creation should be done.
The overall architecture of sample solution
Events flow through Event Grid which is responsible for routing different types of events to the 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 on the Webhook type of event handler which is hosted in Azure Function. Don't get confused, it's an 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 an Azure Function requires some extra work from an 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 do 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 happens immediately during the configuration which means that you need to deploy an application that handles the Webhook handshake before starting the actual configuration. Overall this causes some extra work from an infrastructure creation point of view because you need to have a specific 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
As 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 requests 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 into multiple steps and execute it in a specific order. The webhook application must be deployed before the event handler (subscription) is possible to configure in Event Grid.
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 an Azure DevOps point of view, this deployment can be executed in its own deployment pipeline.
2. Application deployment
The application deployment pipeline deploys a Webhook endpoint which is hosted in Azure Function. The application must manually handle the Webhook handshake.
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 the Webhook handshake validation code is already in place configuration is possible to finalize.
Bicep implementation
This module orchestrates the configuration of the Webhook type of event handler (subscription). Authorization to Azure Function (Webhook endpoint) is handled with Function Key. The function key of the Azure Function is persisted to KeyVault during the deployment of the 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 the 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 the IaC point of view from Github.
Comments