Running Azure Functions on Kubernetes with KEDA
Running Azure Functions on Kubernetes with KEDA
Traditional serverless functions run directly on Microsoft's infrastructure. But what if you want that same event-driven magic inside your own Kubernetes cluster?
Enter KEDA.
KEDA—the Kubernetes-based Event Driven Autoscaler—dictates exactly how your containers should scale based on the number of events waiting to be processed. Scaling from zero to thousands of pods automatically. It reacts to events. Not requests.
This guide shows you how to containerize a modern .NET 8 Azure Function, drop it into a Kubernetes cluster, and let KEDA handle the autoscaling load.
What You Need
- Minikube or any Kubernetes cluster
- Azure Functions Core Tools
- Docker Desktop
- An active Azure Subscription
- VS Code with the Azure Functions extension
Create the Function
Spin up a new .NET 8 project in VS Code. Select the QueueTrigger template. You need an Azure Storage account to trigger this function, so set one up in the Azure portal and grab your connection string.
Containerize It
Generate a Dockerfile right from your project directory.
func init --docker-only
Build the image using the Azure Functions runtime.
docker build . -t queue-function
Tag and push your image to a registry. Docker Hub is easiest, but we'll use Azure Container Registry (ACR) for this example.
docker tag queue-function <registry-name>.azurecr.io/queue-function:latest
docker push <registry-name>.azurecr.io/queue-function:latest
Deploy to Kubernetes
Get KEDA running on your cluster. The modern standard is using Helm.
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --namespace keda --create-namespace
Deploy your function. The Azure Functions CLI reads your configuration, drops the image into the cluster, and automatically configures KEDA's ScaledObject rules for you.
func kubernetes deploy --name queue-function --registry <registry-name>.azurecr.io
Handling ACR Permissions
If you use ACR with a local cluster like Minikube, you will immediately hit permission errors. Your cluster can't pull the image. You fix this by creating an Azure Service Principal scoped with the acrpull role.
ACR_NAME="<your-acr-name>"
SERVICE_PRINCIPAL_NAME="acr-service-principal"
ACR_REGISTRY_ID=$(az acr show --name $ACR_NAME --query id --output tsv)
SP_PASSWD=$(az ad sp create-for-rbac --name $SERVICE_PRINCIPAL_NAME --scopes $ACR_REGISTRY_ID --role acrpull --query password --output tsv)
SP_APP_ID=$(az ad sp list --display-name $SERVICE_PRINCIPAL_NAME --query "[].appId" --output tsv)
Store those credentials in a Kubernetes secret.
kubectl create secret docker-registry acrcred \
--docker-server=$ACR_NAME.azurecr.io \
--docker-username=$SP_APP_ID \
--docker-password=$SP_PASSWD
Patch your default service account so your cluster automatically uses these credentials when pulling images.
kubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name": "acrcred"}]}'
Test the Autoscaler
Want to watch the scaling happen? Limit your function to consuming a single message at a time by updating your host.json.
{
"version": "2.0",
"extensions": {
"queues": {
"batchSize": 1
}
}
}
Rebuild your image, push it, and restart your pods.
Flood the queue to watch KEDA spin up pods. Use this modern C# console app using the updated Azure.Storage.Queues SDK to blast thousands of messages instantly.
using Azure.Storage.Queues;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string connectionString = "<your-connection-string>";
QueueClient queueClient = new QueueClient(connectionString, "kedaqueue");
await queueClient.CreateIfNotExistsAsync();
for (int i = 0; i < 1000; i++)
{
await queueClient.SendMessageAsync($"Message {i}");
}
}
}
