Photo by Desola Lanre-Ologun on Unsplash

Continuous Deployment setup considerations for Azure Function with User assigned Managed Identity.

David Lee
6 min readAug 31, 2023

--

For an Azure Function to run, an Azure Storage Account is required. The simplest setup would be to store the connection string of the Azure Storage Account in the configuration of the Azure Function. However, the connection string is a secret and could cause damage if it is accidently leaked.

With a managed identity, we no longer require a connection string. Instead, the managed identity can be assigned the role of Storage Blob Data Owner and be able to access the Azure Storage Blob.

For my use-case, I have a HTTP Triggered Azure Function that will connect to Azure Table Storage as a backend database. The Azure Function’s job is to serve HTTP Redirects given a key provided in the route and the key references a URL stored in the backend database. The Azure Function will use the Consumption Plan as it is a simple HTTP application.

I would like to automate my deployments using GitHub Actions. The first step would be to create a new environment with bicep. Here, I have decided to create two resource groups. The first resource group contains a Storage Account that serves as the Azure Table Storage backend database as well as a managed identity. The managed identity would be assigned the role of Storage Table Data Contributor to allow access to Azure Table Storage. The second resource group would contain the HTTP Triggered Azure Function along with its storage account.

You may be asking why I didn’t reuse the same storage account for both the backend database and the storage account. I could, however, there are two reasons. One, I would like to decouple my database from operational requirements. For example, what if I needed to further secure access to my Azure Storage account later but this same Azure Storage account is used by my Azure Function and there could be incompatibility. Second, under load, performance could degrade if using the same Azure Storage account.

The second step of the GitHub Action would be to deploy the Azure Function, its associated Azure Storage Account and assign the managed identity I have created earlier to the Azure Function. This technique means we are leveraging User-assigned Managed identity.

Now that we have the background, we can discuss the following considerations.

System-assigned Managed Identity and User-assigned Managed Identity

Both System-assigned Managed Identity and User-assigned Managed Identity will work for the Azure Function. It is the timing of role assignments that would be the difference.

If we are using System-assigned Managed Identity, this means the Azure Function needs to be created first and any role assignments can only happen afterwards.

With a User-assigned Managed Identity, we can create it beforehand. This approach gives us to flexibility to do role-assignment before we run our Bicep that contains the Azure Function and Azure Storage Account and provide it as its identity (as part of the parameters into Bicep).

In my case, the flexibility is I can do role assignment for the User-assigned Managed Identity to access an existing Azure Key Vault, and Azure Table Storage beforehand. The role assignment for the Azure Function’s Storage Account still needs to be done afterwards.

User-assigned Managed Identity to access Blob Storage

The following setting is used by the Azure Function to connect to the Azure Storage account and in particular, Blob Storage with a connection string.

AzureWebJobsStorage

As mentioned earlier, we can assign our Managed Identity was the Storage Blob Data Owner role. For now, we need to tell Azure Function to use this identity. There can be done using the following code in our Bicep.

  identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${appId}': {}
}
}

The appId variable can be replaced by the resource Id of the Managed identity.

Instead of using AzureWebJobsStorage, we will also need to use the following values:

        {
name: 'AzureWebJobsStorage__accountName'
value: str.name
}
{
name: 'AzureWebJobsStorage__credential'
value: 'managedidentity'
}
{
name: 'AzureWebJobsStorage__clientId'
value: appClientId
}

Here, we specify the account name of the storage account, we tell the Azure Function we are using managed identity and provide the client id of the managed identity.

To find the client id, we can use the following Azure CLI command.

az identity show --ids $resourceId --query "clientId" 

As a reminder, do not forget to do the one-time Storage Blob Data Owner role assignment after the Bicep is executed.

Azure Files requirement due to Consumption Plan

Azure Function running on a Consumption Plan (or Elastic Premium) requires access to Azure Files to manage scale-out operations. This is handled by the following settings:

WEBSITE_CONTENTAZUREFILECONNECTIONSTRING

WEBSITE_CONTENTSHARE.

Unlike AzureWebJobsStorage, here, we are unable to use our User-assigned Managed Identity. Instead, we will have to save the connection string in Azure Key Vault as a secret and then reference it. The approach would be to set the connection string of the Storage Account initially during the first deployment. Once the Storage Account has been created, we can set the connection string in Azure Key Vault and then configure it as a reference in our Azure Function settings in a second deployment.

This means our managed identity will require a Key Vault Secrets User role assignment to the Azure Key Vault to access the secret.

Next, in the bicep, we will need to tell our Azure Function that we are using Azure Key Vault with a managed identity. This can be done using the following code in your bicep.

  properties: {
keyVaultReferenceIdentity: appId
...

Again, the appId is the resource Id of the managed identity.

Here, we should mention that there is a chicken and egg problem we will face. Initially when we run our bicep script that contains the Azure Function and its associated Azure Storage Account, there is no connection string yet because there is no storage account. Hence, we cannot set the secret in Azure Key Vault yet.

One approach would be to split up our deployment into 2 steps and have the Azure Storage Account be created first, get and set the connection string to Azure Key Vault, then run the second deployment to create the Azure Function.

A second approach is to still have one deployment but set the Azure Storage connection string the first time the Azure Function is created. Once deployment is completed, get and set the connection string to Azure Key Vault, then run the second deployment to update the Azure Function. Although still a two-step process, we don’t have two separate bicep files to maintain which is just a little bit better.

We should also note that while we are setting up our Azure Function, we should not be deploying any code in the first deployment, otherwise, there could be potential startup issues.

Let’s take a look at the code for bicep.

        {
name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
value: empty(appName) ? 'DefaultEndpointsProtocol=https;AccountName=${str.name};AccountKey=${str.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' : '@Microsoft.KeyVault(VaultName=${sharedKeyVaultName};SecretName=${appStorageConn})'
}
{
name: 'WEBSITE_CONTENTSHARE'
value: 'azurefileshare'
}

Initially I have code that looks to see if an existing Azure Function exist and set the appName variable if it does. If it does not, I assume this is the very first time and will use the actual connection string of the Storage Account, otherwise, I am using a reference to the secret in Key Vault.

User-assigned Managed Identity to access Azure Table Storage

Similar to AzureWebJobsStorage, for the Table Storage, I am able to use the same convention to tell my Azure Function to use User-assigned Managed Identity with the following setting:

        {
name: 'UrlStorageConnection__tableServiceUri'
value: 'https://${appDatabaseNameStr}.table.${environment().suffixes.storage}/'
}
{
name: 'UrlStorageConnection__credential'
value: 'managedidentity'
}
{
name: 'UrlStorageConnection__clientId'
value: appClientId
}

The main difference is I am using __tableServiceUri suffix to specify my table storage.

The UrlStorageConnection is coming from the connection value in the Table Binding as declared in my HTTP Triggered Azure Function.

   public async Task<IActionResult> AddOrUpdateShortUrl(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "{id}")] HttpRequest req,
[Table("Url", Connection = "UrlStorageConnection")] TableClient tableClient,
string id)
{

Conclusion

The idea seemed of using a Managed Identity seemed simple enough, but as we just read, there are a few considerations. There are already many articles out there that talks about using Managed Identity with Azure Function. However, it is easy to miss the details which is my motivation for creating this article and I hope it helps you too.

--

--