Azure Functions Proxies
2018-01-29
Proxies are a nice addition to Azure Functions that give you a subset of features that an application gateway could provide for your function instances. It has request matching capabilities that let you inspect a specific route and send that request to a specific back-end. You can setup matches based on combinations of uri and HTTP method.
Let's explore this with a default function. This is created from the functions portal and it the default for the HTTP trigger.
using System.Net;
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
log.Info("C# HTTP trigger function processed a request.");
// parse query parameter
string name = req.GetQueryNameValuePairs()
.FirstOrDefault(q => string.Compare(q.Key, "name", true) == 0)
.Value;
// Get request body
dynamic data = await req.Content.ReadAsAsync<object>();
// Set name to query string or body data
name = name ?? data?.name;
return name == null
? req.CreateResponse(HttpStatusCode.BadRequest, "Please pass a name on the query string or in the request body")
: req.CreateResponse(HttpStatusCode.OK, "Hello " + name);
}
So we can access this anonymously
You can set this directly in configuration as well by updating the bindings in function.json
{
"bindings": [
{
"authLevel": "anonymous",
"name": "req",
"type": "httpTrigger",
"direction": "in"
},
{
"name": "$return",
"type": "http",
"direction": "out"
}
],
"disabled": false
}
Taking the generated url from the function portal
» Invoke-RestMethod https://yourbackend.azurewebsites.net/api/HttpTriggerCSharp1
Invoke-RestMethod : "Please pass a name on the query string or in the request body"
and you see that the HTTP is not happy with your request as a name
is required in the query string or POST body.
» Invoke-RestMethod https://yourbackend.azurewebsites.net/api/HttpTriggerCSharp1?name="Colter Wall"
Hello Colter Wall
Now we have the falling-out-of-the-chair 'Hello World' Azure Function working.
Personally I'd like to see a File > New > Function code sample to work by default
Now let's add a proxy to see if we can make the simple HTTP trigger execute correctly by a client invocation with no parameters.
From the Function portal it would be created like the following
the more interesting json view of this look like this. This time we're in proxies.json
{
"$schema": "http://json.schemastore.org/proxies",
"proxies": {
"add-name-to-query": {
"matchCondition": {
"route": "/api/HttpTriggerCSharp1"
},
"backendUri": "https://yourbackend.azurewebsites.net/api/HttpTriggerCSharp1",
"requestOverrides": {
"backend.request.querystring.name": "Colter Wall"
}
}
}
}
What we've configured here is a named proxy configuration called add-name-to-query
that is looking for a route match on /api/HttpTriggerCSharp1
in which the request will be ferried over to the uri https://yourbackend.azurewebsites.net/api/HttpTriggerCSharp1
. As it, this is completely pointless however we are enhancing the request by adding a query string parameter called name
with the value of Colter Wall
.
Now we can make that original request again
» Invoke-RestMethod https://yourbackend.azurewebsites.net/api/HttpTriggerCSharp1
Hello Colter Wall
How are we going to make some routing adjustments by having the route match at the root of our functions site
Adding static query string values
{
"$schema": "http://json.schemastore.org/proxies",
"proxies": {
"add-name-to-query": {
"matchCondition": {
"route": "/"
},
"backendUri": "https://yourbackend.azurewebsites.net/api/HttpTriggerCSharp1",
"requestOverrides": {
"backend.request.querystring.name": "Colter Wall"
}
}
}
}
which will let us make a call such as
» Invoke-RestMethod https://yourbackend.azurewebsites.net/
Hello Colter Wall
Function Proxies Templates variables
Thing are getting cleaner but despite how great Colter Wall is I didn't ask for him. Let's see if we can apply a route template into the mix.
{
"$schema": "http://json.schemastore.org/proxies",
"proxies": {
"add-name-to-query": {
"matchCondition": {
"route": "/{name}"
},
"backendUri": "https://yourbackend.azurewebsites.net/api/HttpTriggerCSharp1",
"requestOverrides": {
"backend.request.querystring.name": "{name}"
}
}
}
}
» Invoke-RestMethod https://yourbackend.azurewebsites.net/queens-of-the-stone-age
Hello queens-of-the-stone-age
Function Proxies to route to other sites
You don't need to necessarily have that backend pointed to your function. If you are not a rebot you can head over to one of Runscope's utility sites called https://requestbin.com/ and create a throw away endpoint that will show you what your http requests look like to the server.
Updating the backendUri
property to a requestbin inspector
{
"$schema": "http://json.schemastore.org/proxies",
"proxies": {
"add-name-to-query": {
"matchCondition": {
"route": "/{name}"
},
"backendUri": "https://requestbin.com/19x58nh1?inspect",
"requestOverrides": {
"backend.request.querystring.name": "{name}"
}
}
}
}
and calling the same request will just get you a 200 OK response but on the requestbin page you can see the headers and query string values
RequestBin is handy for light weight request sharing. Httpbin is awesome in a different way in that you can have some control over the response. Again update the backendUri
with a new value of https://httpbin.org/headers
Template values for the backenduri
Templates parameter can also be used in the backendUri
value
{
"$schema": "http://json.schemastore.org/proxies",
"proxies": {
"add-name-to-query": {
"matchCondition": {
"route": "/{name}"
},
"backendUri": "https://httpbin.org/{name}"
}
}
}
Calling a couple of the documented httpbin resources
» Invoke-RestMethod https://yourbackend.azurewebsites.net/headers | ConvertTo-Json
{
"headers": {
"Connection": "close",
"Disguised-Host": "yourbackend.azurewebsites.net",
"Host": "httpbin.org",
"Max-Forwards": "10",
"User-Agent": "Mozilla/5.0,(Windows NT; Windows NT 10.0; en-US),WindowsPowerShell/5.1.16299.98",
"Was-Default-Hostname": "yourbackend.azurewebsites.net",
"X-Arr-Log-Id": "dc4e8792-5823-444f-a76f-8efa20eee6f0",
"X-Arr-Ssl": "2048|256|C=US, S=Washington, L=Redmond, O=Microsoft Corporation, OU=Microsoft IT, CN=Microsoft IT TLS CA 4|CN=*.azurewebsites.net",
"X-Original-Url": "/queens-of-the-stone-age",
"X-Site-Deployment-Id": "yourbackend",
"X-Waws-Unencoded-Url": "/queens-of-the-stone-age"
}
}
» Invoke-RestMethod https://yourbackend.azurewebsites.net/robots.txt
User-agent: *
Disallow: /deny
Multiple Uri Segments
{*restOfPath}
can be used for sub-resources where you have multiple segments in your Uri
{
"$schema": "http://json.schemastore.org/proxies",
"proxies": {
"add-name-to-query": {
"matchCondition": {
"route": "/{*restOfPath}"
},
"backendUri": "https://httpbin.org/{restOfPath}"
}
}
}
And this time we use PowerShell's Invoke-WebRequest so we can extract the links from the response
» (Invoke-WebRequest https://yourbackend.azurewebsites.net/links/3/3).Links
innerHTML : 0
innerText : 0
outerHTML : <A href="/links/3/0">0</A>
outerText : 0
tagName : A
href : /links/3/0
innerHTML : 1
innerText : 1
outerHTML : <A href="/links/3/1">1</A>
outerText : 1
tagName : A
href : /links/3/1
innerHTML : 2
innerText : 2
outerHTML : <A href="/links/3/2">2</A>
outerText : 2
tagName : A
href : /links/3/2
I'm using this feature in Dotnetzero so that I can take a source off of dotnetzero.com/dotnetcli and pass some query string parameters to that the function will return a different command to the user.
A snippet of this looks like the following and the little 'm' magic is where I do a route match of the /dotnetcli
to passed a couple of PowerShell pipeline commands instead of the full parent script. This will just run the wizard portion of the dotnetzero where the dotnet command are wrapped for user input.
{
"$schema": "http://json.schemastore.org/proxies",
"proxies": {
"appcli": {
"matchCondition": {
"route": "/dotnetcli"
},
"backendUri": "https://yourbackend.azurewebsites.net/api/fullapp",
"requestOverrides": {
"backend.request.method": "get",
"backend.request.querystring.clicmd": "Get-DotNetProjects | New-DotNetSolution"
}
}
}
}
Adding values to HTTP request headers
Like because were we added a static query string value we can also work with the request headers
{
"$schema": "http://json.schemastore.org/proxies",
"proxies": {
"add-name-to-query": {
"matchCondition": {
"route": "/{*restOfPath}"
},
"backendUri": "https://httpbin.org/{restOfPath}",
"requestOverrides": {
"backend.request.headers.x-experimental": "8675309"
}
}
}
}
» irm https://yourbackend.azurewebsites.net/headers | ConvertTo-Json
{
"headers": {
"Connection": "close",
"Disguised-Host": "yourbackend.azurewebsites.net",
"Host": "httpbin.org",
"Max-Forwards": "10",
"User-Agent": "Mozilla/5.0,(Windows NT; Windows NT 10.0; en-US),WindowsPowerShell/5.1.16299.98",
"Was-Default-Hostname": "yourbackend.azurewebsites.net",
"X-Arr-Log-Id": "f76a5b15-22fa-4f6b-9486-02d2a3b0da35",
"X-Arr-Ssl": "2048|256|C=US, S=Washington, L=Redmond, O=Microsoft Corporation, OU=Microsoft IT, CN=Microsoft IT TLS CA 4|CN=*.azurewebsites.net",
"X-Experimental": "8675309",
"X-Original-Url": "/headers",
"X-Site-Deployment-Id": "yourbackend",
"X-Waws-Unencoded-Url": "/headers"
}
}
I think that Azure Functions proxies are going to be useful in a different areas. For scenarios where you might not need to use Azure's various gateway solutions or maybe not yet able to justify using API Gateway I think that proxies can provide soutions for a number of scenarios.
Next up we will talk about the Response overrides and where that can help with API consumer development and prototyping.