Azure Cloud Resume Challenge Update: Adding Azure API Management
The Azure Cloud Resume Challenge in most cases seeks to update a visitor counter when a visitor lands on the resume holder's page.
In my first iteration of the Azure Cloud Resume Challenge I wrote about here, I initially created an architecture that looks like this:
As you can see, my implementation allowed all users to effectively invoke the Azure Function responsible for updating the visit counter in Cosmos DB, directly with Anonymous invocation. There were minor CORS protections added in to the Function App to make sure that responses would only be sent back to browser clients that were running from the desired domain in the form of the CDN endpoint. Regardless, there is a need to acknowledge the sub-optimal practice in the first implementation where the users on their browsers are rather 'too close to the metal' being able to call in directly to the Azure Function.
The Azure Function's duties and visibility can be further abstracted away from the user's browser, and we can do this with an API Gateway inside of an Azure API Management Instance.
From the Azure Portal, I created a new API Management instance on the Developer Tier. Then I imported my existing CosmosReadWriter Azure Function as an API from under the APIM Instance>APIs>Add API>Create from Azure Resource>Function App:
Another key point is that we want to have the browser/client code be free of keys of any kind to keep things clean. We won't be using the Ocp-Apim-Subscription-Key header in an API call from the browser's javascript. Instead, we will be allowing all browser clients to freely call the API Endpoint, just without any knowledge of any keys. To stop the API Gateway automatically checking for the default Ocp-Apim-Subscription-Key in each request, we can Untick the tick box for Subscription Required in the Settings for our API and Save:
Then each client IP address will be rate limited to prevent too many requests from each client using the Limit Call Rate policy in Inbound Processing:
The API Gateway will enforce a CORS Policy to make sure that browser based requests came from an allowed origin. In my case, I added both the CDN Endpoint where I know users should be coming from and the Blob Storage Origin Endpoint as part of a CORS policy in Inbound Processing:
The API Gateway will add a known custom header key using the Set headers Policy in Inbound Processing to the request before it gets to the backend (the Azure Function), so that the user's browser never gets to know what this key is and is completely blind to any keys or secrets. This key is completely custom and is SEPERATE from the Ocp-Apim-Subscription-Key and can be thought of being a 'stamp of approval' made by the API Gateway itself:
Then the Javascript call used by the HTML Page is updated to now call to the endpoint of the API now ready in the APIM instance, rather than the old Function URL used before. This new file is uploaded back (and overwrites the existing file) into the $web storage container where the Static Web App runs from:
Here, it is also a good idea to purge the CDN under CDN instance Overview>Purge so that the CDN can be re-hydrated with the latest updates from the origin storage container. Otherwise the CDN will use the default content refresh policy, which may not be immediate to take effect for our need.
Finally, the Azure Function code will be updated to become responsible for adding a check for the existence of a specific request header key that is expected to be added by the API Gateway. This key will also be stored in Key Vault. If this expected key is present on the request coming into the Azure Function verified against the same value stored in Key Vault, then the Azure Function can be assured that the request went through the API Gateway and went through the appropriate checks to be granted permission to reach the Azure Function:
The Azure Function code can be updated to look like this instead (other classes):
The entire static web app can now be sending requests to the API Gateway in Azure APIM rather than sending requests directly to the Azure Function.
The new architecture would look like this now: