How to Upload Blobs to Azure Storage from an Azure Function with Azure Managed Identities (Part 3)

In this 3 part series we are going to learn a few methods for developing an Azure Function that uploads blobs to Azure Storage using the new Azure Blob Storage SDK and the new Azure Identity SDK’s DefaultAzureCredential. We’ll first get everything running locally without any cloud dependencies via a Storage Emulator. We’ll then run the same code locally, but configure it to upload blobs to Azure Storage using a Service Principal. And lastly, we’ll learn how to deploy an Azure Function to Azure, that uses Azure Storage and Managed Identities. The three different setups allow you to iteratively develop Azure Functions without the cost and inner-loop dev cylce overhead of deploying everything to Azure on every code change.

Code: The code for this series can be found here: https://github.com/jongio/azure-blob-functions-managedid

Part 1: Local Function with Storage Emulator (local function, local storage)
Part 2: Local Function with Azure Storage and Service Principal (local function, cloud storage)
Part 3: Azure Function with Azure Storage and Managed Identity (cloud function, cloud storage)

Azure Function with Azure Storage and Managed Identity (cloud function, cloud storage)

In Parts 1, we create a local function, wrote blobs to local storage and then in Part 2 we configured it to upload blobs to Azure Storage using a Service Principal. In Part 3 we are going to deploy our Azure Function to Azure and use Managed Identities instead of a Service Principal. A Managed Identity is a Service Principal under the hood, but Azure takes care of regular maintainance of it and enables you to deploy your app with zero code or configuration changes. You just use DefaultAzureCredential in your app and it will automatically pick up the Managed Identity and use it to authenticate with other Azure services.

Azure Setup

  1. Create Azure Functions Storage Account

Azure Functions store metadata and logs in an Azure Storage Account. You can use the same storage account that you created earlier to upload your blobs to or you can create a new one.

Options:

Option 1. Use the same storage account that you created above and skip this step.

Option 2. Create a new storage account for Function metadata using the following command:

az storage account create -n FUNCTIONS_STORAGE_ACCOUNT_NAME -g RESOURCE_GROUP_NAME --kind StorageV2 --sku Standard_LRS

Parameters:

  • FUNCTIONS_STORAGE_ACCOUNT_NAME This is the name of the storage account that you want to store your Azure Functions metadata in.
  • RESOURCE_GROUP_NAME The name of the resource group that you created earlier.
  • --sku - List of available SKUs can be found here: SKU Types
  1. Create App Service Plan

We are going to put our Function in an App Service Plan because that enables log streaming (among other features), whereas a Consumption based Azure Function (Linux) does not - at least not yet.

az appservice plan create -n APP_SERVICE_PLAN_NAME -g RESOURCE_GROUP_NAME --is-linux --sku B1

Parameters:

  • APP_SERVICE_PLAN_NAME A name that you choose to give your app service plan.
  • RESOURCE_GROUP_NAME The name of the resource group that you created earlier.
  • --is-linux You can remove this parameter if you want to use Windows
  • --sku You can also choose --sku F1 for a free app service plan, but you are only allowed one per subscription. You can find all of the App Service Plan SKUs here: az appservice plan create
  1. Create Azure Function App

This is the actual Function app that we will upload our Function app project.

az functionapp create -n FUNCTION_APP_NAME -g RESOURCE_GROUP_NAME --storage-account FUNCTIONS_STORAGE_ACCOUNT_NAME --plan APP_SERVICE_PLAN_NAME --runtime dotnet --os-type Linux

Parameters:

  • FUNCTION_APP_NAME A name that you choose to call your function app.
  • RESOURCE_GROUP_NAME The same resource group name that you created earlier
  • FUNCTIONS_STORAGE_ACCOUNT_NAME If you only created one storage account for both Azure Function metadata and blobs, then use that single account. If you created multiple accounts, then enter the name of the one that you want your metadata stored in here.
  • APP_SERVICE_PLAN_NAME The name of the app service plan you created earlier.
  • --runtime We are using dotnet in this post, but you can use any of the available options. Obviously your code will need to change to reflect that option.
  • --os-type You can choose Windows and it should work exactly the same way.
  1. Set Function App Settings

As you saw above, our code depends on two environment variables:

var account = Environment.GetEnvironmentVariable("AZURE_STORAGE_ACCOUNT");
var container = Environment.GetEnvironmentVariable("AZURE_STORAGE_CONTAINER");

For local deployments, those are configured in the local.settings.json file. When we deploy to Azure we’ll need those settings in the Function app. Use the following commands to update those settings.

az functionapp config appsettings set -n FUNCTION_APP_NAME -g RESOURCE_GROUP_NAME --settings "AZURE_STORAGE_ACCOUNT=BLOB_STORAGE_ACCOUNT_NAME" "AZURE_STORAGE_CONTAINER=azfuncblobs" "AZURE_STORAGE_HOST=blob.core.windows.net" "FUNCTIONS_EXTENSION_VERSION=~3"

Parameters:

  • FUNCTION_APP_NAME A name that you choose to call your function app.
  • RESOURCE_GROUP_NAME The same resource group name that you created earlier
  • BLOB_STORAGE_ACCOUNT_NAME The storage account name that you would like to store the blobs in.
  • AZURE_STORAGE_CONTAINER You can also change the name of the container. For this post it is azfuncblobs.
  • FUNCTIONS_EXTENSION_VERSION Right now, when you create an Azure Function app via the CLI it creates it using v2 of the function runtime, but we are working with v3, so we need to manually update that setting. We have a PR open here for this, so you may not need to set that parameter. You can run this command to check its current value: az functionapp config appsettings list -n FUNCTION_APP_NAME -g RESOURCE_GROUP_NAME

You will see the following output after you execute that command:

{
  "name": "AZURE_STORAGE_ACCOUNT",
  "slotSetting": false,
  "value": "jongstorage"
},
{
  "name": "AZURE_STORAGE_CONTAINER",
  "slotSetting": false,
  "value": "azfuncblobs"
},
{
  "name": "FUNCTIONS_EXTENSION_VERSION",
  "slotSetting": false,
  "value": "~3"
},
  1. Update the Container to .NET Core 3

When you create an Azure Function app via the CLI it doesn’t use the 3.0 version of the dotnet:x-appservice docker image. So we have to manually update it. We have a bug filed for this issue here: Can’t change functions version for Linux App Service Plan app V2 => V3 #5559.

Update 2/28/2020: This issue should resolve this https://github.com/Azure/azure-cli/pull/12317, which will come in the next CLI release.

To view the current settings:

az functionapp config container show -n FUNCTION_APP_NAME -g RESOURCE_GROUP_NAME

Which will return this until the bug is fixed:

[
  {
    "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
    "slotSetting": false,
    "value": "true"
  },
  {
    "name": "DOCKER_CUSTOM_IMAGE_NAME",
    "value": "DOCKER|mcr.microsoft.com/azure-functions/dotnet:2.0-appservice"
  }
]

To update to the correct docker image:

az functionapp config container set -n FUNCTION_APP_NAME -g RESOURCE_GROUP_NAME --docker-custom-image-name mcr.microsoft.com/azure-functions/dotnet:3.0-appservice

Which will output the correct settings:

[
  {
    "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
    "slotSetting": false,
    "value": "false"
  },
  {
    "name": "DOCKER_CUSTOM_IMAGE_NAME",
    "value": "DOCKER|mcr.microsoft.com/azure-functions/dotnet:3.0-appservice"
  }
]

Parameters:

  • FUNCTION_APP_NAME A name that you choose to call your function app.
  • RESOURCE_GROUP_NAME The same resource group name that you created earlier

Assign Managed Identity to Function App

You have two options when it comes to choosing a Managed Identity: System Assigned or User Assigned. With System Assigned Azure will create the identity on your behalf, with User Assigned, you first create the identity and then assign it. User assigned is useful when you want to reuse the identity across multiple Azure resources.

Option 1: Assign a System Assigned Managed Identity to Function App

This will enabled a Managed Identity for the Function App and assign it the Storage Blob Data Contributor role. You don’t have to individually create a Managed Identity and assign it roles - you can do it all in this one command.

az functionapp identity assign -n FUNCTION_APP_NAME -g RESOURCE_GROUP_NAME --role ba92f5b4-2d11-453d-a403-e96b0029c9fe --scope /subscriptions/SUBSCRIPTION_ID

Parameters:

  • FUNCTION_APP_NAME The function app name you created earlier.
  • RESOURCE_GROUP_NAME The resource group you created earlier.
  • SUBSCRIPTION_ID - The subscription id that the function is in. Run az account show to get this value.
  • --role - The GUID ba92f5b4-2d11-453d-a403-e96b0029c9fe is the ID for the Storage Blob Data Contributor role. You can find all of the built-in Azure roles here: Built-in roles for Azure resources

Option 2: Assign a User Assigned Managed Identity to Function App

With this option, you first create the Managed Identity and then assign it to the Function App. This is useful if you want to reuse the identity for multiple resources, but Azure still manages it the way it manages system assigned identities.

This requires a few extra steps because the az functionapp identity assign command does not support user assigned identities yet. We have a bug filed here: Add support for User Assigned Identity for functionapp and webapp #9887

  1. Create the User Assigned Managed Identity
az identity create -g RESOURCE_GROUP_NAME -n USER_ASSIGNED_IDENTITY_NAME

Parameters:

  • RESOURCE_GROUP_NAME The resource group you created earlier.
  • USER_ASSIGNED_IDENTITY_NAME A unique identity name that you create.

Once this successfully completes, you can skip ahead to the “Deploy Function” section below.

  1. Get the User Assigned Managed Identity Metadata

We are going to need the resource id and client id in a minute, so let’s get them now.

az identity list -g RESOURCE_GROUP_NAME

Parameters:

  • RESOURCE_GROUP_NAME The resource group you created earlier.
  1. Assign the Storage Blob Data Contributor Role to the User Assigned Identity
az role assignment create --assignee USER_ASSIGNED_IDENTITY_CLIENT_ID --role ba92f5b4-2d11-453d-a403-e96b0029c9fe

Parameters:

  • USER_ASSIGNED_IDENTITY_CLIENT_ID This is the “clientId” property that you retrieved in the last step.
  • --role - The GUID ba92f5b4-2d11-453d-a403-e96b0029c9fe is the ID for the Storage Blob Data Contributor role. You can find all of the built-in Azure roles here: Built-in roles for Azure resources
  1. Assign the User Assigned Identity to the Function App

We know this is messy and should just be part of the az functionapp identity assign call. We have a bug filed. For now, you have to use this if you want to use the CLI. You could also use the Azure portal or ARM if you’d like.

NOTE: USER_ASSIGNED_IDENTITY_RESOURCE_ID below is the complete path to the Resource in Azure, not a GUID, not a clientId, it looks something like this: /subscriptions/25fd0362-aa79-488b-b37b-d6e892009fdf/resourcegroups/jgfnrg1/providers/Microsoft.ManagedIdentity/userAssignedIdentities/jgfnmanid1

You can use the az functionapp update command:

az functionapp update -n FUNCTION_APP_NAME -g RESOURCE_GROUP_NAME --set identity="{\"type\": \"UserAssigned\", \"userAssignedIdentities\": {\"USER_ASSIGNED_IDENTITY_RESOURCE_ID\": {}}}"

Or you can directly edit the Azure resource using the az resource update command:

az resource update -n FUNCTION_APP_NAME -g RESOURCE_GROUP_NAME --resource-type "Microsoft.Web/sites" --set identity="{\"type\": \"UserAssigned\", \"userAssignedIdentities\": {\"USER_ASSIGNED_IDENTITY_RESOURCE_ID\": {}}}"

Here’s an example of what the actual command looks like:

az functionapp update -n jgfnapp1 -g jgfnrg1 --set identity="{\"type\": \"UserAssigned\", \"userAssignedIdentities\": {\"/subscriptions/25fd0362-aa79-488b-b37b-d6e892009fdf/resourcegroups/jgfnrg1/providers/Microsoft.ManagedIdentity/userAssignedIdentities/jgfnmanid1\": {}}}"

Parameters:

  • FUNCTION_APP_NAME The function app name you created earlier.
  • RESOURCE_GROUP_NAME The resource group you created earlier.
  • USER_ASSIGNED_IDENTITY_RESOURCE_ID The complete Azure resource id for the user assigned identity, which you retrieved earlier.
  1. Set Environment Variables

Because you are using a User Assigned Managed Identity, we are going to have to manually set some Environment Variables to explicitly tell Azure which Identity to use:

az functionapp config appsettings set -n FUNCTION_APP_NAME -g RESOURCE_GROUP_NAME --settings "AZURE_CLIENT_ID=USER_ASSIGNED_IDENTITY_CLIENTID" "AZURE_TENANT_ID=TENANT_ID"

Parameters:

  • FUNCTION_APP_NAME The function app name you created earlier.
  • RESOURCE_GROUP_NAME The resource group you created earlier.
  • USER_ASSIGNED_IDENTITY_CLIENTID This is the client id for the user assigned managed identity that you created earlier
  • TENANT_ID This is the Azure tenant that your subscription lives in - you can find this with az account show.

If you don’t do this, then you’ll likely see the following exception later in the logstream. (See below for info on how to view the logstream)

{"statusCode":400,"message":"Unable to load requested managed identity.","correlationId":"fc7504dd-c16c-4526-b8a8-667f6463d306"}

Now that you have your Managed Identity configured it is time to deploy our Function to Azure.

Deploy Azure Function to Azure

We are now FINALLY ready to deploy the Azure Function to Azure and test it out.

  1. Deploy the Function

Open a terminal and execute the following command to deploy your local function to Azure. There are many ways to deploy an Azure Function, for this example we’ll use the Azure Function Core Tools

func azure functionapp publish FUNCTION_APP_NAME

Parameters:

  • FUNCTION_APP_NAME The function app name you created earlier.

Notes:

  • If you see the following exception, then try again in a few minutes. I got this error. Waiting a few minutes, ran it again and it worked:
Getting site publishing info...
Creating archive for current directory...
Uploading 3.19 MB [###############################################################################]
Upload completed successfully.
Deployment completed successfully.
Response status code does not indicate success: 400 (Bad Request).
  • This step can take a few minutes and may not work the first time. If you don’t get back a URL, then keep trying until you do. You may see output like this last doesn’t include the URL. Just try again.
  1. Test the Function

Once it has been successfully deployed, it will output the Function URL

Getting site publishing info...
Creating archive for current directory...
Uploading 2.97 MB [###############################################################################]
Upload completed successfully.
Deployment completed successfully.
Functions in jgfnapp1:
    upload - [httpTrigger]
        Invoke url: https://jgfnapp1.azurewebsites.net/api/upload?code=CR

Go to that URL to execute the function and verify that a file was uploaded.

Notes:

  • If it doesn’t succeed right away, give it a few minutes to cycle, restart the app, and try again.
  • If you see the following error, you’ll want to verify that your Managed Identity is configured correctly.
2020-01-31T23:38:19.117 [Error] Executed 'func1' (Failed, Id=09062b19-c7e8-45f6-a9bc-d828d47adbb2)
Environment variables not fully configured. AZURE_TENANT_ID and AZURE_CLIENT_ID must be set, along with either AZURE_CLIENT_SECRET or AZURE_USERNAME and AZURE_PASSWORD. Currently set variables [  ]

You can view the Managed Identities assigned to your Function by executing the following command:

az resource list -n FUNCTION_APP_NAME -g RESOURCE_GROUP_NAME

Which will output something like this:

{
    "id": "/subscriptions/25fd0362-aa79-488b-b37b-d6e892009fdf/resourceGroups/jgfnrg1/providers/Microsoft.Web/sites/jgfnapp1",
    "identity": {
      "principalId": "ca9937fe-2967-443b-99c1-c480bf6000dd",
      "tenantId": "72f988bf-86f1-41af-91ab-2d7cd011db47",
      "type": "SystemAssigned",
      "userAssignedIdentities": null
    },
    "kind": "functionapp,linux,container",
    "location": "westus2",
    "managedBy": null,
    "name": "jgfnapp1",
    "plan": null,
    "properties": null,
    "resourceGroup": "jgfnrg1",
    "sku": null,
    "tags": null,
    "type": "Microsoft.Web/sites"
}
  1. Debug the Function

If you get any failures, or if you just want to view the logs you can view the Function App logstream.

You have a couple of options:

Option 1: VS Code Debugging

If you have VS Code and the VS Code Functions extension, you can just hit F5 and step through your code.

Option 2: Azure Functions VS Code Extension

Open the Azure VS Code extension, expand FUNCTIONS, find your Function, right-click and select “Start Streaming Logs”. That will open the Azure portal and you can view your logs.

You can install the VS Code extension here: Install Azure Functions VS Code Extension (Optional)

Option 3: Azure Functions Core Tools

You can also view the logstream via the terminal using the Azure Function Core Tools

func azure functionapp logstream FUNCTION_APP_NAME

Parameters:

  • FUNCTION_APP_NAME The function app name you created earlier.

Option 4: Azure Portal

Go to the Azure Portal and find your Azure Function. You can run it from within the browser and see return codes and exceptions.

  1. Verify Success with Storage Explorer

Open Storage Explorer and navigate to: Subscription -> Storage Accounts -> Storage Account -> Blob Containers -> azfuncblobs

Verify that your file has been successfully uploaded.

Conclusion

We covered a lot in this series. My hope is that learning about the 3 different combinations of local and cloud dev with Managed Identities and the new Azure Identity DefaultAzureCredential will help you be more productive.

Part 1: Local Function with Storage Emulator (local function, local storage)
Part 2: Local Function with Azure Storage and Service Principal (local function, cloud storage)
Part 3: Azure Function with Azure Storage and Managed Identity (cloud function, cloud storage)

Please leave a comment below if you found this post helpful or need help with any of this.

Jon