Azure Functions with Managed Identities - Part 1
Hi All! Today we’ll steer away a bit from Design Patterns and talk about Azure Managed System Identities, or MSI, and how they can be leveraged in Azure Function Apps.
So what are exactly Managed Identities?
First of all, they are a broader term, that includes both System and User Assigned Identities. The difference is easy: System Assigned Identities are generated by Azure, while User Assigned Identity on the other hand are created by the user.
Being Managed however, means that Azure will completely take away all the burden of dealing with credentials. And this is one of the most important differences with non-managed Identities:
There is no need at all to generate certificates or secrets/passwords.
This basically means that we don’t have to manually (or via scripts) go into AAD, create an Application, generate the credentials, store them in a safe place (aka a Key Vault in most cases), deal with rotations and so on…
Now that we have cleared this, why should we use a User Assigned Identity over a System one? Let’s see.
System MSIs are somehow part of the same resource they belong to. Say that you’re creating a new Azure Function App and you choose a System MSI. This identity will have the same lifetime of the Function App, which also means that if the App is deleted, the Identity will be deleted to.
This might not be a problem for small applications or prototypes. But if for example you are deleting and recreating the App as part of your CI/CD pipeline, the Identity will change every time.
In case you are assigning roles or granting access to other resources, this might be an issue if you’re not carfeul and you’ve automated everything.
Now, User-Assigned MSIs instead don’t have this problem. They are a completely different Azure Resource, and they can even be shared across multiple Azure Services. To put it in another way, the Identity is managed separately from the resources that use it.
Let’s see how we can write a simple ARM template to deploy a Function App that leverages a User-Assigned MSI:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"parameters": {
"location": {
"type": "string",
"defaultValue": "eastus"
},
"environment": {
"type": "string",
"defaultValue": "dev"
},
"serviceName": {
"type": "string",
"defaultValue": ""
}
},
"variables": {
"funcAppName": "[toLower(concat(parameters('serviceName'), '-', parameters('environment')))]",
"identityName": "[toLower(concat(parameters('serviceName'), '-', parameters('environment')))]",
"identityResourceId": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName'))]",
"storageAccountName": "[toLower(concat(parameters('serviceName'), parameters('environment')))]",
"storageAccountResourceId": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]",
"planName": "[toLower(concat(parameters('serviceName'), '-', parameters('environment')))]",
"planResourceId": "[resourceId('Microsoft.Web/serverfarms', variables('planName'))]"
},
"resources": [
// storage account
{
"type": "Microsoft.Storage/storageAccounts",
"kind": "StorageV2",
"apiVersion": "2021-08-01",
"name": "[variables('storageAccountName')]",
"location": "[parameters('location')]",
"sku": {
"name": "PremiumV2"
},
"properties": {
"supportsHttpsTrafficOnly": true,
"allowCrossTenantReplication": false
}
},
// plan
{
"type": "Microsoft.Web/serverFarms",
"apiVersion": "2015-08-01",
"name": "[variables('planName')]",
"sku": {
"Tier": "PremiumV2",
"Name": "P1v2"
},
"location": "[parameters('location')]",
"properties": {
"name": "[variables('planName')]",
"workerSize": "1",
"workerSizeId": "3",
"numberOfWorkers": "1"
}
},
// user-assigned identity
{
"type": "Microsoft.ManagedIdentity/userAssignedIdentities",
"name": "[variables('identityName')]",
"apiVersion": "2018-11-30",
"location": "[parameters('location')]"
},
// function app
{
"name": "[variables('funcAppName')]",
"kind": "functionapp",
"type": "Microsoft.Web/sites",
"apiVersion": "2021-03-01",
"dependsOn": [
"[variables('planResourceId')]",
"[variables('identityResourceId')]",
"[variables('storageAccountResourceId')]"
],
"location": "[parameters('location')]",
"identity": {
"type": "UserAssigned",
"userAssignedIdentities": {
"[variables('identityResourceId')]": {}
}
},
"properties": {
"enabled": true,
"httpsOnly": true,
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('planName'))]",
"siteConfig": {
"location": "[parameters('location')]",
"alwaysOn": true,
"http20Enabled": true,
"minTlsVersion": "1.2",
"ftpsState": "Disabled",
"use32BitWorkerProcess": false
}
}
}
]
}
This ARM template translates into this:
Let’s see what’s happening here. In the parameters
block, we have defined what we expect from the caller, namely the location
for the resources, their environment
(eg. DEV, QA, PROD) and the name of the service we’re working on.
We use those parameters to build a few variables
, basically the names of the resources we’re going to provision along with their resourceId
. We will use the resourceIds
to build the dependency graph between the various services.
At this point we proceed with the definition of the resources. The bare minimum necessary for an Azure Function is a serverFarm
and a storageAccount
. But now that we have a specific resource for the Identity, we can assign it to the Function using this block:
"identity": {
"type": "UserAssigned",
"userAssignedIdentities": {
"[variables('identityResourceId')]": {}
}
}
And that’s it! The next time we will expand a bit this template and add access to a Key Vault.
Ciao!