In the following article I am going to show you how to build a dynamic http proxy using Azure Functions and truly little code. If you feel like reading code rather than reading an article about code, go straight to GitHub and have it your way.
You might be asking yourself what is a dynamic http proxy and why would I need one? You could think of it as a service registry that also allows you to proxy the http calls to the services it registers, so a dynamic http proxy, DHP or DHTTPP in short.
One possible use case for such a service is between your frontends and your backends. Regardless of how fragmented or dynamic your backend services landscape is, the DHP makes it possible for you to define a single, predictable API surface for the frontends to consume. The frontends also become simpler, by only having to know about one endpoint. You could accomplish the same using a solution like API Management of course, but while for each modified or added backend you need to reconfigure the API Management, the DHP reconfigures itself.
If you used Azure Functions before, you might have stumbled upon Azure Functions Proxies. They are somewhat like API Management and can be used to achieve the same goal of a single API surface for all your services. But just like API Management, they are not dynamic, you would have to add any additional route and potentially redeploy the functions app. I will not be using them for the DHP.
The idea is simple, every discoverable service registers itself with the DHP. After registering, the service will be accessible through the proxy.
Let us start with the service registration. When registering itself a service needs to provide a name, a version and the host where it can be found. I guess a name and a host would have sufficed, but it is nice to have API versioning out-of-the-box and it adds little in terms of extra code to the final solution. In the following code listing you can see how the service registration function looks like:
The function is as simple as it gets. The DHP accepts service registrations at /api/register and the payload for the registrations needs to be a ServiceEntry delivered in the body of the post method. Once it gets the ServiceEntry, it simply saves it to a table in an Azure Table Storage. I will leave service deregistration's (delete) and service queries (get) up to you, since they are not mandatory for the DHP.
You might have noticed that before saving to the table, we set the RegisteredAt property of the ServiceEntry to the current datetime in UTC. This can be useful in multiple ways. If you make your services register only once, then this will only tell you when that happened of course. However, if you make your services register once every few minutes, then suddenly this tells you which services are up, and which are down. Nice and simple!
In the following code listing you can see the ServiceEntry class:
As you can see, I am using the service name and version as partition and row keys, which feels very natural, doesn’t it? Without the service version we would have had to make one of the keys up, which would have been a waste.
This is all for service registration, let us see how the proxy functions look like. It is only one function and you can see it in the following code listing:
It is a simple HTTP triggered function, that accepts all HTTP verbs. Using Azure Functions binding expressions, it maps the partition and row keys of the table input binding to the name and version variables extracted from the route. This way, the runtime does the job of retrieving the matched ServiceEntry for us. Next, it builds the target endpoint using the service host and the rest of the path extracted from the route, adjusts the HttpRequestMessage and sends it using the injected IProxyService. You can see the implementation of the IProxyService in the next code listing:
I bet you thought this will be complicated. Here we are just wrapping the good old HttpClient to make the call to our proxied service. The only thing worth mentioning here is the use of HttpCompletitionOption.ResponseHeadersRead which means the operation should complete as soon as a response is available and headers are read. Since we are proxying the response, there is no need to wait until all the content is read.
As promised, little code, yet quite capable!