Our Chief Architect, Jimmy Bogard, is hosting a webinar on building messaging endpoints in Azure. It’s on Dec. 11 at 1:00pm CST if you want to join! He also wrote a whole series of articles on the subject—so we thought we’d share his latest on using Azure Functions—complete with code. Check it out:
Deploying message endpoints in containers, eventually out to Azure Container Instances, is fairly straightforward, but close to Infrastructure-as-a-Service. You can scale, but you can’t auto-scale, and even if you use Kubernetes, you can’t scale based on exceeding your lead time SLA (time from when a message enters the queue until it is consumed).
But what about the serverless option of Azure Functions? Can this offer a better experience for building a messaging endpoint?
So far, the answer is “not really,” but it will highly depend on your workload or needs. The programming and hosting model is vastly different than containers or web jobs, so we first need to understand how the function will get triggered.
Choosing a trigger
Something needs to kick off our endpoint, and for this, we have a couple of choices. The most basic choice is the Azure Service Bus binding for Azure Functions. I mention “basic,” because it is. We’re not really building an endpoint here, but a single function. It may seem like a small difference, but it’s really not. When building endpoints, the handler is just one piece of the puzzle—there’s also the host configuration, logging, tracing, error handling, and beyond that, complex messaging patterns.
The other option is a pre-release version of the NServiceBus support for Azure Functions, which dramatically alters the development model of functions itself and sends us back to developing message handlers (instead of merely functions).
First, let’s look at the out-of-the-box binding.
Azure Service Bus binding
With Azure Functions’ own special SDK package, we’re creating a “Functions” project and choosing the “Azure Service Bus” trigger. All this really does behind the scenes is create a project with the correct NuGet package references:
Yes, it’s a little odd: we’re adding a package for “WebJob,s” but this is a Functions application. That’s because the WebJobs triggers and Functions triggers share the same infrastructure, but with a different hosting/deployment model.
In any case, we can now create a function with Many Attributes. Here’s one for a simple request/response:
We have to declare the function name, as an attribute, the trigger, as an attribute, and the return value also as an attribute. Service Bus client takes care of deserializing the message from JSON (assuming the content type of the message was application/json).
Functions (or the Azure Service Bus client) don’t understand the concept of “request/response,” or “pub/sub” for that matter, so we have to build these concepts on top. To subscribe to an event, we need to set up the topic and subscription inside of the broker.
There’s no support for request/response, return addresses, or correlated replies. If we want to “reply” back to the receiver, we’d need to create a client, pick off some reply address from the headers, and generate and send the message.
In the above example, I’ve hardcoded the reply queue, NsbAzureHosting.Sender, so it’s not even following the correct message pattern. With request/reply, the receiver should be ignorant of the sender, just as modern email clients are.
So while all this works, we don’t get the more advanced features of a full message endpoint and all the patterns of the Enterprise Integration Patterns book. We have to roll a lot ourselves.
We also get only very primitive retry capabilities: retries are immediate and once exhausted the message goes to the dead-letter queue. With NServiceBus, we get immediate and delayed retries—very helpful when we’re using some external resource whose downtime won’t get resolved within milliseconds.
With all this in mind, let’s look at the NServiceBus function support.
I won’t rehash the entire sample, but there are some key differences in this setup versus a “normal” functions setup. Azure Functions doesn’t have a lot of the extensibility support that NServiceBus does, so it won’t give you things like Outbox, deferred messages, idempotent receivers, sagas, and so on. So NServiceBus gets around this by still hosting an “endpoint” and delegating the message handling to the endpoint from inside your function:
The endpoint is what processes the message inside a full execution pipeline, so you can focus on building out full message handlers, instead of just functions:
Now inside of our message handler, we get the full IMessageHandlerContext, and not just the ExecutionContext of a function, which is fairly limited. Now we can reply, publish, defer, set timeouts, kick-off sagas, all inside the full-featured NServiceBus message endpoint.
Neither of these options are great, but for very simple message handlers, an Azure Function can suffice. While an Azure Function isn’t close to a “PaaS Message Endpoint,” it is close to a “PaaS Message Handler,” and that might be sufficient for your needs.