Microsoft recently announced the ability to run Azure Functions on Azure Container Apps (in preview). This is a big improvement over the current serverless option for Azure Functions as it allows us to leverage the benefits of microservices and the features of Azure Container Apps like DAPR and KEDA.
Behind the scenes, KEDA is used as the event driven auto scaler for the Functions. You still need to spin up an Azure Function for managing your functions however they execute within the containers (compute environment). This is also a lot more cost efficient as you can use the Azure Container Apps consumption plan and only pay for what you use.
These are the triggers currently supported whilst in preview that allow dynamic scaling from zero instances:
- Azure Storage Queue
- Azure Service Bus
- Azure Event Hubs
- Kafka Trigger
I'm certain that more triggers will be available down the line as KEDA itself has a huge library of various trigger types.
I will run through a quick demo which is also available on the Microsoft docs. It's a PowerShell Function that responds to an http request (API, Get) and returns a response if the request is successful. Pretty simple but you can develop whatever function you desire in all the languages currently supported by Azure Functions (.net, Java etc). I also ran some load Tests using Azure Load Testing (JMeter) to see how the scaling performs in real life so you can check that out at the end of the post.
This is what we will be getting up and running today. The exception being that we won't be using virtual network integration or private endpoints just to speed things up but in a production environment you definitely want to enforce all the network traffic to remain within a virtual network in Azure.
You will need the following if you want to run this yourself
- Azure Functions Core Tools version 4.x.
- Install the .NET 6 SDK.
- Azure CLI version 2.4 or a later version.
- An Azure Container Registry to push the container images to.
Before we deploy anything to Azure, let's get our Function set up locally and testing it using Postman (you can also just use a web browser).
1: Initiate the Functions project
func init --worker-runtime powershell --docker
The docker feature flag will generate a .Dockerfile for the project which we will use later to wrap up the code into a Docker image and push it up to the container registry.
2: Let's add a Function to our project.
func new --name HttpTriggerContainerApp --template "HTTP trigger" --authlevel anonymous
3: Once that's done, we need to test it locally to make sure our Function executes properly before we containerise it. To start the Function locally run:
You should see the URL for the Function in your terminal: http://localhost:7071/api/HttpTriggerContainerApp
4: Now let's call our API using by suppliying a query parameter in the Http Get request. If you haven't got Postman you can just call pass the parameter via your web browser:
Now we should have a response if the call was successful:
Now we know the Function executes using the Http trigger locally we can move on to build our image and getting the same Function executing in Azure Container Apps.
Our Docker image was automatically generated when we created our Functions project so all we need to do is build it, tag it and push it up to our container registry.
You will need Docker to do this step. I am using Podman which is an alternative for building OCI images but the commands are the same. Just replace
dockerin the command line. Make sure you cd into the root directory of the project.
You will need to replace the
acr_name with your registry in Azure. Make sure you have the admin credentials enabled on the registry. You can do this via the CLI
az acr update -n <acr_name> --admin-enabled true
1: Build the image:
podman build --tag <acr_name>.azurecr.io/azurefunctionsimage:v1.0.0 .
2: Run the image locally and test it again via podman or your web browser:
podman run -p 8080:80 -it <acr_name>.azurecr.io/azurefunctionsimage:v1.0.0
3: Our container is now listening on port 8080 so here is the new URL to test with: http://localhost:8080/api/HttpTriggerContainerApp?name=Functions
Now we know our image works when running in a container we can push it our container registry.
4: Log in to your Azure Container Registry:
podman login <acr_name>.azurecr.io -u <acr_username> -p <acr_password)
5: Push the image to the container registry by running the following:
podman push <acr_name>.azurecr.io/azurefunctionsimage:v1.0.0
Now that our image is ready we can proceed to setting up our Azure environment and deploying our Function App on to Azure Contaiener Apps.
1: Run the below commands to register the required namespaces and to upgrade the Container Apps CLI extension:
az extension add --name containerapp --upgrade -y az provider register --namespace Microsoft.Web az provider register --namespace Microsoft.App az provider register --namespace Microsoft.OperationalInsights
2: Create the resource group for the resources in Azure:
az group create --name AzureFunctionsDemo-rg \ --location australiaeast
3: Create the Container App environment:
az containerapp env create --name azure-functions-demo-cae \ --resource-group AzureFunctionsDemo-rg \ --location australiaeast
4: Create the storage account required for the Function Apps. This is used to store all of the Function code used in the runtime:
az storage account create --name auefunctionsdemo001 \ --location australiaeast \ --resource-group AzureFunctionsDemo-rg \ --sku Standard_LRS
4: Create the Function App. You will notice here we need to provide the container registry details. This is what allows Azure Function to deploy our Function on to the Container App Environment:
az functionapp create --name azure-functions-demo-func \ --storage-account auefunctionsdemo001 \ --environment azure-functions-demo-cae \ --resource-group AzureFunctionsDemo-rg \ --functions-version 4 --runtime powershell \ --image <acr_name>.azurecr.io/azurefunctionsimage:v1.0.0 \ --registry-server <acr_name>.azurecr.io \ --registry-username <acr_username> \ --registry-password <acr_password>
In a production scenario you would use a managed identity to authenticate to the container registry.
5: Let's retrieve the endpoint of our function so we can call this and test that we get the expected response from the API:
az functionapp function show \ --resource-group AzureFunctionsDemo-rg \ --name azure-functions-demo-func \ --function-name HttpTriggerContainerApp \ --query invokeUrlTemplate
Now that we have the endpoint, we can test it the same way we did when we deployed it locally. I am using Postman to do this.
We can now see that we get the exact same response with the Function running on Azure container apps!
This was a relatively simple setup that shows the concept of running Functions on Container Apps. The benefits are that we can use the feature of Container Apps on our Functions like Keda and DAPR. Additionally, it seems it's cheaper to run Azure Container Apps with vNet integration that running a standard App Service Plan or Azure Functions premium which are required in order to integrate into your own virtual network.
P.S You can configure your Container App Environment in your own virtual network allowing your Function app to communicate privately over RFC1918 to any other Azure services you have or private endpoints.