Connecting an Azure Function to Azure API Management
published 03-10-2023
Why is this important?
When working with serverless functions we need to decide how to expose the functions to our clients. If you're working with event driven architectures this may not apply but when creating REST endpoints with Azure Functions we need to make a choice. We can expose each of our function apps directly to the internet or we can use Azure API Management to expose our functions.
If we expose the functions directly then each function api host url will need to be known to the client application and changes to these urls will require changes to the client applications or at least their configurations.
If we use Azure API Management then we can expose a single url to the client application and changes to the function route can be abstracted away from the client application. If you are working with a single function then it may not be worth the effort to use Azure API Management but as your application grows it will be helpful.
Getting Started
Prerequisites
If you choose to follow along with this article you will need the following:
- Azure Subscription
- Azure CLI
- Azure Function Core Tools
Part 1: Create Resources
The first step is to create our function, the focus of this article is the connection between the function and the api management instance so the content of the function is not important. I will create a basic hello world function but feel free to make your own as complex or simple as you would like.
Step 1: Creating the Azure Function
Create Azure Function with the Azure Function Core Tools. This will create a new directory for your function app and initialize the function app. It will also create a new function with the name "HelloWorld" and set the authentication level to function, this will require a function code to be passed through the query parameters to execute the function.
mkdir MyFunctionApp && cd MyFunctionApp # Create a directory for your function app
func init --language "C#" --worker-runtime dotnet --name MyFunctionApp # Initialize the function app
func new --template "HttpTrigger" --name "HelloWorld" --authlevel "function" # Create a new function
Here is where you can choose the complexity of your function. If you want to follow along with a simple hello world
function here is the code.
[FunctionName("HelloWorld")]
public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", Route = "ping")] HttpRequest req, ILogger log)
{
var responseMessage = new
{
Message = "Hello World"
};
return new OkObjectResult(responseMessage);
}
You can run and test the function locally with the following command.
NOTE: The function code will be used when the function is deployed to Azure, it's not needed for local executions
func start
Step 2: Provision and Deploy Azure Function App
Now that we have a working function we can deploy it to Azure. We will use the Azure CLI to provision the resources and the Azure Function Core Tools to deploy the function. This script will also create an Application Insights instance for the function app, this is done by default when you create a function app through the az functionapp create
command.
Param(
[Parameter(Mandatory = $true)]
[String]
$ResourceGroupName,
[Parameter(Mandatory = $true)]
[String]
$StorageAccountName,
[Parameter(Mandatory = $false)]
[String]
$StorageAccountType = "Standard_LRS",
[Parameter(Mandatory = $true)]
[String]
$FunctionAppName,
[Parameter(Mandatory = $true)]
[String]
$Location
)
# ------------------------------------------------------
# Variables
# ------------------------------------------------------
$RESOURCE_GROUP_NAME = $ResourceGroupName
$LOCATION = $Location
$FUNC_APP_NAME = $FunctionAppName
$FUNC_STORAGE_ACCOUNT = $StorageAccountName.Replace("-", "").ToLower()
$FUNC_STORAGE_ACCOUNT_TYPE = $StorageAccountType
# ------------------------------------------------------
# Provision Resource Group
# ------------------------------------------------------
az group create `
--name $RESOURCE_GROUP_NAME `
--location $LOCATION
# ------------------------------------------------------
# Provision Storage Account
# ------------------------------------------------------
az storage account create `
--name $FUNC_STORAGE_ACCOUNT `
--resource-group $RESOURCE_GROUP_NAME `
--location $LOCATION `
--sku $FUNC_STORAGE_ACCOUNT_TYPE
# ------------------------------------------------------
# Provision Function App
# ------------------------------------------------------
az functionapp create `
--resource-group $RESOURCE_GROUP_NAME `
--name $FUNC_APP_NAME `
--storage-account $FUNC_STORAGE_ACCOUNT `
--consumption-plan-location $LOCATION `
--runtime dotnet `
--assign-identity [system] `
--functions-version 4 `
--os-type Windows `
--https-only true
once the script has completed you can deploy the function with the following command. Be sure this command is run in the directory with the function and replace the values with your own function app name.
func azure functionapp publish <Your Azure Function App Name>
Step 3: Provision Azure API Management instance
Now that we have a function app we can provision an Azure API Management instance. We will use the Azure CLI to provision the resources. This will create a consumption (serverless) tier instance of Azure API Management, this can take a bit of time to provision as its a heavy resource.
Param(
[Parameter(Mandatory = $true)]
[String]
$ResourceGroupName,
[Parameter(Mandatory = $true)]
[String]
$ApiManagementName,
[Parameter(Mandatory = $true)]
[String]
$Location,
[Parameter(Mandatory = $true)]
[String]
$PublisherEmail,
[Parameter(Mandatory = $true)]
[String]
$PublisherName
)
# ------------------------------------------------------
# Variables
# ------------------------------------------------------
$RESOURCE_GROUP_NAME = $ResourceGroupName
$APIM_NAME = $ApiManagementName
$APIM_SKU = "Consumption"
$APIM_LOCATION = $Location
$APIM_PUBLISHER_EMAIL = $PublisherEmail
$APIM_PUBLISHER_NAME = $PublisherName
# ------------------------------------------------------
# Provision Resource Group
# ------------------------------------------------------
az group create `
--name $RESOURCE_GROUP_NAME `
--location $LOCATION
# ------------------------------------------------------
# Provision API Management Instance
# ------------------------------------------------------
az apim create `
--resource-group $RESOURCE_GROUP_NAME `
--name $APIM_NAME `
--location $APIM_LOCATION `
--sku-name $APIM_SKU `
--enable-managed-identity true `
--publisher-email $APIM_PUBLISHER_EMAIL `
--publisher-name $APIM_PUBLISHER_NAME
Part 2: Connect Function App to API Management
We will now create an API in API Management and connect it to our function app, at this point we are not using any security on either the function app or the API in API Management, but we will add that here in step 2.
Step 1: Create API Management Operation Policy XML
Azure API Management allows you to create policies to control the behavior of your API requests. In this case we will create a policy that will add the function code to the url so we don't have to pass it in ourselves, in fact we don't even need to know what the function code is. This will force all traffic through the API Management instance.
We will add the function code to the xml file in the following step but first we need to create the xml template. Create a new file called policy.xml
and add the following xml.
<policies>
<inbound>
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
You don't have to use that file name, the important thing is the file content and that you know the path to the file so you can reference it in the next step.
Use this link if you want to learn more about API Management Policies.
Step 2: Connect API Management to Function
The following script performs the following steps:
- Creates an API in API Management
- Creates an operation in the API
- Creates a policy for the operation and deploys it through a Powershell created ARM template; this is the part that may look complex.
Param(
[Parameter(Mandatory = $true)]
[String]
$ResourceGroupName,
[Parameter(Mandatory = $true)]
[String]
$ApiManagementName,
[Parameter(Mandatory = $true)]
[String]
$FunctionAppName,
[Parameter(Mandatory = $true)]
[String]
$ApiManagementPolicyPath
)
# ------------------------------------------------------
# Variables
# ------------------------------------------------------
$RESOURCE_GROUP_NAME = $ResourceGroupName
$APIM_NAME = $ApiManagementName
$FUNCTION_APP_NAME = $FunctionAppName
$API_NAME = "helloworld-api"
# ------------------------------------------------------
# Get Function App Base URI
# ------------------------------------------------------
$DEFAULT_HOST_NAME = az functionapp show `
--resource-group $RESOURCE_GROUP_NAME `
--name $FUNCTION_APP_NAME `
--query "defaultHostName" `
--output tsv
$FUNCTION_APP_BASE_URI = "https://${DEFAULT_HOST_NAME}/api"
# ------------------------------------------------------
# Add Hello World API to API Management Instance
# ------------------------------------------------------
az apim api create `
--resource-group $RESOURCE_GROUP_NAME `
--service-name $APIM_NAME `
--api-id $API_NAME `
--display-name "Hello World API" `
--path "helloworld" `
--service-url $FUNCTION_APP_BASE_URI `
--protocols "https" `
--description "This is a simple Hello World API" `
# ------------------------------------------------------
# Add /Ping Operation to Hello World API
# ------------------------------------------------------
$APIM_OPERATION_ID = az apim api operation create `
--display-name "Get Hello World" `
--method "GET" `
--resource-group $RESOURCE_GROUP_NAME `
--service-name $APIM_NAME `
--api-id $API_NAME `
--url-template "/ping" `
--query "name" `
--output tsv `
# ------------------------------------------------------
# Get Function key for use in API Management Policy
# ------------------------------------------------------
$FUNCTION_KEY = az functionapp keys list `
--resource-group $RESOURCE_GROUP_NAME `
--name $FUNCTION_APP_NAME `
--query "functionKeys.default" `
--output tsv
# ------------------------------------------------------
# Build and Deploy Query Param Policy for Function Key
# ------------------------------------------------------
[System.Xml.XmlDocument]$policy = Get-Content $ApiManagementPolicyPath
$InBoundXml = '<base/><set-query-parameter name="code" exists-action="override"><value>' + $FUNCTION_KEY + '</value></set-query-parameter>'
$policy.policies.inbound.InnerXml = $InBoundXml
$policy.Save($ApiManagementPolicyPath)
[string]$PolicyAsString = Get-Content $ApiManagementPolicyPath
[PSCustomObject]$POLICY_RESOURCE = @{
type = "Microsoft.ApiManagement/service/apis/operations/policies"
apiVersion = "2022-08-01"
name = "${APIM_NAME}/${API_NAME}/${APIM_OPERATION_ID}/policy"
properties = @{
format = "xml"
value = $PolicyAsString
}
}
$TEMPLATE = @{
'$schema' = "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"
contentVersion = "1.0"
resources = @($POLICY_RESOURCE)
}
touch ./azure/apimPolicyDeploy.json
$TEMPLATE | ConvertTo-Json -Depth 10 | Out-File ./azure/apimPolicyDeploy.json
az deployment group create `
--name "apim-op-${APIM_OPERATION_ID}-policy-deploy" `
--resource-group $RESOURCE_GROUP_NAME `
--template-file ./azure/apimPolicyDeploy.json
At this point the you can test the API at with the following url structure:
curl <API Management Gateway URL>/<API Path>/<Operation Path>
If you followed along the with the code above the API Path and the Operation Path would be helloworld
and ping
respectively, however your values may differ depending on how you named your API and operation.
NOTE: at least at the time of writing this the Azure CLI cannot define a policy for API Management operations, so we have to use an ARM template to do it.
I hope this helps you get started with Azure API Management and Azure Functions. If you have any questions or comments please feel free to reach out to me.