Locally debugging an Azure Function triggered by Azure Event Grid

When working with Azure Event Grid, say for something like Azure Blob Storage Events, it's useful to be able to debug your handling code locally before going through the process of pushing up to its final resting place (in Azure, of course 😉).

However, when routing Storage Events to a custom web endpoint you're met with a bit of a conundrum - how can I get the event to hit my local machine so I can debug my code locally?

This can be accomplished quite easily using a free, publicly-available tool called ngrok. Ngrok effectively creates a tunnel to your local machine on HTTP ports (80, 443) that's accessible via the public internet. Upon running it, you're automatically assigned a subdomain of ngrok.io, and any traffic to that endpoint is routed to your local machine.

How can we use this in conjunction with Event Grid? With our friend Azure Functions! By creating an HTTP-triggered Azure Function, running it locally, and setting breakpoints in our code we can inspect the payload coming from Event Grid and also ensure we're reacting properly to it. Doing so requires a couple of important steps, though.

  1. Make sure you run ngrok correctly. When launching ngrok, you have the option of specifying the port it forwards to locally. For .Net Azure Functions this is, by default, 7071 so you need to give that to ngrok when you run it. Second, you need to specify the host header as Azure Functions runtime doesn't like seeing the request came in to 'ngrok.io' but is now hitting its endpoint hosted on 'localhost'. To do this, you specify the host-header parameter. All-in-all, your ngrok launch command should look something like this (for Azure Functions .Net development):
    ngrok http -host-header=localhost 7071

Upon running this, you'll get a console window:

Take note of the https endpoint given to you by ngrok; Event Grid allows only https endpoints as targets for subscriptions to topics.

  1. Your Function needs to properly handle the SubscriptionValidationEvent sent out by Event Grid when a subscription endpoint is created. You can read more about this here, but suffice to say here's the code you need to do this:
 var payloadFromEventGrid = JToken.ReadFrom(new JsonTextReader(new StreamReader(await req.Content.ReadAsStreamAsync())));
dynamic eventGridSoleItem = (payloadFromEventGrid as JArray)?.SingleOrDefault();
if (eventGridSoleItem == null)
{
    return req.CreateErrorResponse(HttpStatusCode.BadRequest, $@"Expecting only one item in the Event Grid message");
}

if (eventGridSoleItem.eventType == @"Microsoft.EventGrid.SubscriptionValidationEvent")
{
    log.Verbose(@"Event Grid Validation event received.");
    return new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new StringContent($"{{ \"validationResponse\" : \"{((dynamic)payloadFromEventGrid)[0].data.validationCode}\" }}")
    };
}

That's all there is to setup! The final step is, of course, to put the correct URL in to your Event Grid subscription. For the example above, and an Azure HTTP-triggered function called "TestEventGrid" that URL would look like https://5abd9dff.ngrok.io/api/TestEventGrid

Put this URL in to the Event Grid subscription for an Azure Storage Event account as shown:

As soon as you hit 'Create', you'll see your Function get hit with the SubscriptionValidationEvent, then the subscription should be created successfully. You can then drop a blob in to any container in that storage account and you'll see the events fire & hit your locally-running Azure Function!

A couple of caveats:

  1. ngrok endpoints change with each run of ngrok. The nice thing is if you leave ngrok running and hibernate/suspend your machine then wake it back up, ngrok will attempt to reconnect and grant you the same address. Very handy. However if the address changes, or you stop/restart ngrok, don't forget to update your Event Grid Subscription's endpoint URL!
  2. Don't forget to have your function running locally when you create the subscription! If Event Grid doesn't get a 200 OK back with the right payload, it'll error out creating the subscription for you and you'll have to do it again.