I recently implemented a change in KEDA (currently evaluated as a potential pull request), consisting of leveraging managed identities in a more granular way, in order to adhere to the least privilege principle. While I was testing my changes, I wanted to use managed identities not only for KEDA itself but also for the Azure Functions I was using in my tests. I found out that although there are quite a few docs on the topic, none is targeting AKS:
You can find many articles showing how to grab a token from an HTTP triggered function, or using identity-based triggers, but in the context of a function hosted in Azure itself. It's not rocket science to make this work in AKS but I thought it was a good idea to recap it here as I couldn't find anything on that.
Quick intro to managed identities
Here is a quick reminder for those who would still not know about MI. The value proposition of MI is: no password in code (or config). MI are considered best practices because the credentials used by identities are entirely managed by Azure itself. Workloads can refer to identities without the need to store credentials anywhere. On top of this, you can manage authorization with Azure AD (single pane of glasses), unlike shared access signatures and alternate authorization methods.
AKS & MI
For MI to work in AKS, you need to enable them. You can find a comprehensive explanation on how to do this here. In a nutshell, MI works the following way in AKS:
An AzureIdentity and AzureIdentityBinding resource must be defined. They target a user-assigned identity, which is attached to the cluster's VM scale set. The identity can be referred to by deployments through the aadpodbinding annotation. The function (or anything else) container makes a call to the MI system endpoint http://169..., that is intercepted by the NMI pod, which in turn, performs a call to Azure Active Directory to get an access token for the calling container. The calling container can present the returned token to the Azure resource to gain access.
Using the right packages for the function
The packages you have to use depend on the Azure resource you interact with. In my example, I used storage account queues as well as service bus queues. To leverage MI from within the function, you must:
- use the Microsoft.Azure.WebJobs.Extensions.Storage >= 5.0.0
- use the Microsoft.Azure.WebJobs.Extensions.ServiceBus >= 5.0.0
- use the Microsoft.NET.Sdk.Functions >= 4.1.0
Note that the storage package is not really an option because Azure Functions need an Azure Storage account for the most part.
Passing the right settings to the function
Azure functions takes their configuration from the local settings and from their host's configuration. When using Azure Functions hosted on Azure, we can simply use the function app settings. In AKS, this is slightly different as we have to pass the settings through a ConfigMap or a Secret. To target both the Azure Storage account and the Service Bus, you'll have to define a secret like the following:
data:
AzureWebJobsStorage__accountName: <base64 value of storage account name>
ServiceBusConnection__fullyQualifiedNamespace: <base64 value of the service bus FQDN>
FUNCTIONS_WORKER_RUNTIME: <base64 value of the function language>
apiVersion: v1
kind: Secret
metadata:
name: <secret name>
---
data:
AzureWebJobsStorage__accountName: <base64 value of the storage account name>
ServiceBusConnection__fullyQualifiedNamespace: <base64 value of the service bus FQDN>
FUNCTIONS_WORKER_RUNTIME: <base64 value of the function code>
apiVersion: v1
kind: Secret
metadata:
name: misecret
---
apiVersion: aadpodidentity.k8s.io/v1
kind: AzureIdentity
metadata:
name: storageandbushandler
annotations:
aadpodidentity.k8s.io/Behavior: namespaced
spec:
type: 0
resourceID: /subscriptions/.../resourceGroups/.../providers/Microsoft.ManagedIdentity/userAssignedIdentities/storageandbushandler
clientID: <client ID of the user-assigned identity>
---
apiVersion: aadpodidentity.k8s.io/v1
kind: AzureIdentityBinding
metadata:
name: storageandbushandler-binding
spec:
azureIdentity: storageandbushandler
selector: storageandbushandler
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: busandstoragemessagehandlers
labels:
app: busandstoragemessagehandlers
spec:
selector:
matchLabels:
app: busandstoragemessagehandlers
template:
metadata:
labels:
app: busandstoragemessagehandlers
aadpodidbinding: storageandbushandler
spec:
containers:
- name: secretlessfunc
image: stephaneey/secretlessfunc:dev
imagePullPolicy: Always
envFrom:
- secretRef:
name: misecret
---
[FunctionName("StorageQueue")]
public void StorageQueue([QueueTrigger("myqueue-items", Connection = "AzureWebJobsStorage")]string myQueueItem, ILogger log)
{
log.LogInformation($"C# Queue trigger function processed: {myQueueItem}");
}
[FunctionName("ServiceBusQueue")]
public void ServiceBusQueue([ServiceBusTrigger("myqueue-items", Connection = "ServiceBusConnection")] string myQueueItem, ILogger log)
{
log.LogInformation($"C# Queue trigger function processed: {myQueueItem}");
}