Using Azure API Management for Azure Functions

When dealing with resources in Azure that can be called when the right credentials are known by a calling client, it can be easy to simply grant direct access to the cloud resource itself. In the case of Azure Functions the common scenario is to give the client/caller/user a Function URL with a Function key associated with the function for authorisation. But it really does not have to be this way, the client that has been given a Function URL may act maliciously for example and create an unlimited number of requests to the Azure Function. In my case back in Overhead Part 1, I was giving the away the full Function URL to the client (in the form of a Twilio webhook). In essence, the link between my Twilio Environment and Azure Environment was as follows:

The webhook calls the Azure Function using the Function URL. This gave too much direct/concrete access to trigger the Azure Function 

A better way would be not to give the FunctionUrl to the client at all. The client app does not necessarily need to be aware that we are using Azure Functions to fetch the data it wants, thus creating more abstraction. Enter Azure API Management!.

Azure API Management acts as a first line front door that our users and client apps call into to interact with our processing resources in Azure such as our Functions that can serve back results (if need be), back through API Management and back to the calling client again. The diagram would now look like following with some changes to key components discussed further below:

Azure APIM can act as a safety guard against incoming traffic and then direct it to our backend service (an Azure Function)

In my implementation of API Management as structured above, I used the Developer Tier(explained later**) for API Management and I used the Ocp-Apim-Subscription Key as part of verifying the calling client (the authentication processes with APIM are beyond scope here but an ideal way is to use Client certificate authentication). This Ocp-Apim-Subscription Key can be given to our users/client apps to use as part of their requests that are sent to Azure APIM rather than sending requests to the Azure Function directly. The Ocp-Apim-Subscription Key is sent specifically as an HTTP header in the request to APIM. In my Twilio environment, having an extra HTTP header to send to APIM meant I had to utilise a Twilio Function (Twilio's serverless functions) in order to make this request. The default config for a SMS Webhook call as used in Overhead Part 1 in the Twilio dashboard does not allow the adding of extra HTTP headers. Using Twilio Functions with Node.js, we can write the function like this, and customise our HTTP POST call to APIM with the headers we need:


//Custom HandleSMS/process Twilio function
//Add dependencies
const axios = require('axios');
const qs = require('qs');

exports.handler = function (context, event, callback) {
 
  const instance = axios.create({
    baseURL: '[APIM Request URL to Added Function - find this in Azure APIM Test area]',
    timeout: 3000,
    headers: {
      'Ocp-Apim-Subscription-Key': '[Your Ocp-Apim Key Here - find this in Azure APIM Test area]',
      'Host': '[NAME_OF_APIM_HOST_NAME - find this in Azure APIM Test area]',
      'Content-Length': 140, //max sms size in bytes is 140 bytes
      'Content-Type': 'application/x-www-form-urlencoded',
    },
  });
  
  //add form-data properties we want, including the 'body' object required by our ProcessMessage Function in Azure.
  const data = qs.stringify({
    subject: 'From Twilio Function',
    body: `${event.Body}`, //the sms is passed in event.Body
  });

  //Twilio Function requires at least one callback
  instance
    .post('/',data)
    .then(() => {

      console.log(JSON.stringify("done"));
      return callback(null, "done");
    })
    .catch((error) => {
      console.error(error);
      return callback(error);
    });
};
My Twilio Function for calling into Azure APIM. Note that Twilio account funds are required to trigger serverless Twilio Functions

I added an axios dependency to create the custom http client instance to have my custom header in place. Then I added the qs dependency to easily construct the form data that is sent as the payload to the endpoint. Dependencies can be added in the Twilio dashboard here:

The axios and qs dependencies can be added from this section in the Twilio Function View. I called my function HandleSMS/process

After saving the Twilio function code and deploying it using the Deploy All button in Twilio, I configured my new HandleSMS/process function as the function that is triggered when the Twilio number receives an SMS:

The config in Twilio is setup to use a Twilio Function instead, compared to a direct call to the Azure Function in Overhead Part 1

In the Twilio dashboard, the wizard here allows a Function which will actually just set the Twilio Function's URL as the webhook endpoint after saving.

In Azure, the APIM instance I created has the Azure Function added as a backend service that can be consumed by callers with the ocp-apim-subscription key. This can be done in the portal by by going to Azure APIM Instance > APIs > Create from Azure Resource > Function App. When you locate your Function App, remember that the only selectable Functions from your Function App will be Functions that have an HTTP trigger as the input trigger. In my case, for the purpose of limiting the call rate of a caller we can add a 'rate limit by key' policy on the inbound requests as shown below:

My APIM view. Note that in my implementation, the 'ProcessMessage' Function is sequentially called through the starting Orchestrator Function as part of Durable Functions. So within the APIM config, we target the orchestrator function. 

We can add the rate limit by choosing '+Add policy' and using the GUI or opening the Inbound Processing panel Editor shown as '</>' in Inbound processing and adding a limit of 10 calls per 30 seconds for example:

<policies>
    <inbound>
        <base />
        <set-backend-service id="apim-generated-policy" backend-id="[your azure function name is autopopulated here]" />
        <rate-limit-by-key calls="10" renewal-period="30" counter-key="@(context.Subscription?.Key ?? "anonymous")" />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

After saving, this setup now effectively means that calling into the Function through the new configured APIM instance now stops too many calls. This can be tested with the APIM Request URL that can be found in the portal from APIM Instance > APIs > [target API] > [target Post operation] > Test > Request URL:

The APIM Request URL and the ocp-apim subscription key can be found in the Test area in the APIs blade

With the valid APIM details (Request URL, Host and ocp apim sub key) we can decide to 'misbehave' here by continuously sending in requests in Postman for example (shown below):

Example using Postman, calling the APIM Request URL to our Function, with the 'body' property needed in the Function added as form data, and the ocp-apim-subscription key added as one of the headers, if more than 10 calls are made in 30seconds, then new calls gets rejected and they never get to the Azure Function

As seen with Postman above, when we call into the APIM Request URL to get to the Function (with the ocp apim key as header 'Ocp-Apim-Subscription-Key', and a 'body' property as form-data), the APIM instance now applies rules to incoming traffic, such as rate limiting, or any other policies that we may configure to make sure that requests to our backend services have been sanitised as needed. This behaviour enforced by APIM applies to my Twilio Function in kind and prevents it from calling into the Azure Function too many times and helps minimise and soften it's access to the Azure Function.

**Note, the rate limiting policy is only available from the Developer tier of APIM and higher. The Developer tier of APIM costs at least £0.05 per hour at the time of writing.

Cover Photo by  Laila Gebhard / Unsplash