Skip to content

Instantly share code, notes, and snippets.

@paulyuk
Last active June 7, 2024 19:37
Show Gist options
  • Save paulyuk/e75e9a8650a4b05c8f4c923805c86a5d to your computer and use it in GitHub Desktop.
Save paulyuk/e75e9a8650a4b05c8f4c923805c86a5d to your computer and use it in GitHub Desktop.
Converting a Function App to Managed Identity - 101

Converting a Function App to Managed Identity - 101

Overview

This shows how to take a simple Function App (e.g. the output of our client tools templates for a trigger or binding) or a simple sample and "modernize" it from using ConnectionStrings and secrets in favor of Managed Identity and RBAC.

Assumptions

  • User-assigned managed identity is prefered. Beware if you do not select an identity to use in code (ClientId), it will default to system assigned.
  • local development will make Connections to local emulators if they exist, and otherwise will make remote cloud connections to resource using identity based connections. Some resources like Service Bus, AI Cognitive, Open AI, and Datalake will never have local emulation.
  • Key Vault should be avoided unless your sample is working with a 3rd party service or resource (e.g. Facebook, Google, OpenAi.org). in that case you should use a managed identity to list/read keys from the keyvault, but what you need to do will be topic of more advanced guidance.
  • for automation we prefer Bicep for resource creation and Github Actions for CI/CD
  • AzureWebJobsStorage and your trigger's Identity Connection (I'll call it StorageConnection) will for now share the same storage account connection.

Scenario - blob trigger

In this scenario we will first have a blob trigger function which is the output of our client tools and templates.
The tool will run by default with local.settings.json config set up to use a local emulator (aka.ms/azurite).
Then we will reconfigure the local function to connect to the remote Storage dependency, and test that it works. This will not ship in the sample, but it is useful for testing pre-deployment to ensure your RBAC and identities are configured right. Last we will deploy the app to Azure (dev-test environment) and we will push App Settings that reconfigure the identity based connection.

Create a default Blob Trigger Function (Connection Strings)

VS Code

  1. In a new terminal, create a new folder for your code, e.g.
cd ~/src
mkdir secure-sample
  1. Open in VS Code
cd secure-sample
code .
  1. Create a new Function

CMD + SHFT + P to show command palette -> Azure Function: Create Function

Choose the default folder it prompts you with which should match ~/src/secure-sample in step 1.

Choose your favorite language and version.

Choose Blob Storage Trigger (this example) or color outside the lines and pick any other trigger.

Keep hitting enter to take all defaults (emulator, connection name, storage container name, etc)

The function is now created.

  1. Inspect local.settings.json

It probably looks like this:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "238a05_STORAGE": "UseDevelopmentStorage=true"
  }
}
  1. Inspect your function code file, which might be BlobTrigger1.cs

Note how the Connection name is set. This will derive all future config settings.

[Function(nameof(BlobTrigger1))]
        public async Task Run([BlobTrigger("samples-workitems/{name}", Connection = "238a05_STORAGE")] Stream stream, string name)

Above the Connection value is 238a05_STORAGE which is not intuitive. Let's change it to StorageConnection now.

[Function(nameof(BlobTrigger1))]
        public async Task Run([BlobTrigger("samples-workitems/{name}", Connection = "StorageConnection")] Stream stream, string name)
  1. Now edit the config to match the Connection name and enable local environment
{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "StorageConnection": "UseDevelopmentStorage=true"
  }
}
  1. in a new terminal, start azurite (this example uses docker, but VS Code Azurite extension is ok too)
docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite
  1. run the app

Either press F5 to Run

or in the terminal:

func start

Now there is a working, password free local only template.

Change local function to use remote Storage account with identity

  1. Create a new Storage account or pick an existing

Use either the portal or the CLI:

az group create --name rg-secure-sample --location eastus2

az storage account create \
    --name strpaulyukstorage \
    --resource-group rg-secure-sample \
    --location eastus2 \
    --sku Standard_LRS

Take note of the json output created from this storage account, e.g.

  "networkRuleSet": {
    "bypass": "AzureServices",
    "defaultAction": "Allow",
    "ipRules": [],
    "ipv6Rules": [],
    "resourceAccessRules": null,
    "virtualNetworkRules": []
  },
  "primaryEndpoints": {
    "blob": "https://strpaulyukstorage.blob.core.windows.net/",
    "dfs": "https://strpaulyukstorage.dfs.core.windows.net/",
    "file": "https://strpaulyukstorage.file.core.windows.net/",
    "internetEndpoints": null,
    "microsoftEndpoints": null,
    "queue": "https://strpaulyukstorage.queue.core.windows.net/",
    "table": "https://strpaulyukstorage.table.core.windows.net/",
    "web": "https://strpaulyukstorage.z20.web.core.windows.net/"
  },
  1. change the settings in local.settings.json to leverage your Connection name of StorageConnection plus __PropertyName suffixes.

Suggestion: Use the Azure Functions Triggers and Bindings page for your particular service, go to the Identity section, and learn the required properties and role assignments. E.g. this is what you would reference for Azure Storage Trigger

Leverage this info to reverse engineer the config settings you need for the identity connection in local.settings.json (and later in your Function's App Settings. For this storage example we land on:

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "AzureWebJobsStorage__blobServiceUri": "https://strpaulyukstorage.blob.core.windows.net/",
    "AzureWebJobsStorage__queueServiceUri": "https://strpaulyukstorage.queue.core.windows.net/",
    "StorageConnection__blobServiceUri": "https://strpaulyukstorage.blob.core.windows.net/",
    "StorageConnection__queueServiceUri": "https://strpaulyukstorage.queue.core.windows.net/"
  }
}

As a self check there should be zero secrets in the config still. Only endpoint uri's or names/namespaces should be here. All safe.

  1. Grant your interactive signed in Azure account (e.g. paulyuk@microsoft.com in my case) data read/write/contribute access now to the storage account

Even though you are the creator owner of the storage account, that only makes your account super user for management tasks. Data access is still needed.

In the portal use the Access control (IAM tab) to grant the following roles documented here

  1. Rerun the local function app to test its use of identity based connection to cloud Azure Storage using identity

F5 or func start again. there should be no 403 access errors and you should be able to test uploading a blob to the container in step 1.

Note this step will most likely run your function in VS Code or terminal in the context of the Azure CLI signed in identity. You can always az login to login or az account show to verify.

Deploy the function app and configure identity and RBAC there

  1. Create and Deploy the function app using your favorite method, VS code, CLI, hopes & dreams, etc.
az functionapp create \
     --resource-group \
     --rg-secure-sample \
     --name blobuploader-<UNIQUESTRING> \
     --storage-account strpaulyukstorage \
     --flexconsumption-location eastus2 \
     --runtime dotnet-isolated \
     --runtime-version 8.0

func azure functionapp publish blobuploader-<UNIQUESTRING>
  1. Once deployed, edit the app settings to match what you had in local settings exactly - names and values.

  2. Create a user assigned managed identity in another resource group, say rg-my-identities

Use the portal or this CLI command

az group create --name rg-my-identities --location eastus2

az identity create --name blobfunction-identity --resource-group rg-my-identities

Take note of the clientId in the output of this command.

  1. Set blobfunction-identity as the User-assigned identity for your function in FunctionApp -> Identities

  2. In the Storage account, grant this User-Assigned identity the same exact permissions done above or using the following roles documented here. But note, now it is imperative we have __clientId and __credential values

Local.settings.json baseline

    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "AzureWebJobsStorage__blobServiceUri": "https://strpaulyukstorage.blob.core.windows.net/",
    "AzureWebJobsStorage__queueServiceUri": "https://strpaulyukstorage.queue.core.windows.net/",
    "AzureWebJobsStorage__credential": "managedidentity",
    "AzureWebJobsStorage__clientId": "10f91864-9cc2-4d96-9a26-33c1eee284ac",
    "StorageConnection__blobServiceUri": "https://strpaulyukstorage.blob.core.windows.net/",
    "StorageConnection__queueServiceUri": "https://strpaulyukstorage.queue.core.windows.net/",
    "StorageConnection__credential": "managedidentity",
    "StorageConnection_clientId": "10f91864-9cc2-4d96-9a26-33c1eee284ac"

Using VS Code you can run: CMD + SHIFT + P -> Azure Functions: Upload Local Settings... to easily uploda.

Or use the portal blade for the Function App -> Settings -> Environment Variables -> App Settings..

Or can be created via Az CLI:

az functionapp config appsettings set \
    --name blobuploader-<UNIQUE> \
    --resource-group rg-secure-sample \
    --settings \
    "AzureWebJobsStorage__blobServiceUri=https://strpaulyukstorage.blob.core.windows.net/" \
    "AzureWebJobsStorage__queueServiceUri=https://strpaulyukstorage.queue.core.windows.net/" \
    "AzureWebJobsStorage__credential=managedidentity" \
    "AzureWebJobsStorage__clientId=10f91864-9cc2-4d96-9a26-33c1eee284ac" \
    "StorageConnection__blobServiceUri=https://strpaulyukstorage.blob.core.windows.net/" \
    "StorageConnection__queueServiceUri=https://strpaulyukstorage.queue.core.windows.net/" \
    "StorageConnection__credential=managedidentity" \
    "StorageConnection__clientId=10f91864-9cc2-4d96-9a26-33c1eee284ac"
    
func azure functionapp publish blobuploader-<UNIQUE>

Which converts to this Portal or ARM json

[
  { .. },
  {
    "name": "AzureWebJobsStorage__blobServiceUri",
    "value": "https://strpaulyukstorage.blob.core.windows.net/",
    "slotSetting": false
  },
  {
    "name": "AzureWebJobsStorage__clientId",
    "value": "10f91864-9cc2-4d96-9a26-33c1eee284ac",
    "slotSetting": false
  },
  {
    "name": "AzureWebJobsStorage__credential",
    "value": "managedidentity",
    "slotSetting": false
  },
  {
    "name": "AzureWebJobsStorage__queueServiceUri",
    "value": "https://strpaulyukstorage.queue.core.windows.net/",
    "slotSetting": false
  },
  {
    "name": "StorageConnection__blobServiceUri",
    "value": "https://strpaulyukstorage.blob.core.windows.net/",
    "slotSetting": false
  },
  {
    "name": "StorageConnection__clientId",
    "value": "10f91864-9cc2-4d96-9a26-33c1eee284ac",
    "slotSetting": false
  },
  {
    "name": "StorageConnection__credential",
    "value": "managedidentity",
    "slotSetting": false
  },
  {
    "name": "StorageConnection__queueServiceUri",
    "value": "https://strpaulyukstorage.queue.core.windows.net/",
    "slotSetting": false
  }
]
  1. restart the function for good measure. and test it!
@lilyjma
Copy link

lilyjma commented Jun 4, 2024

Another assumption - System Assigned MI is used by default. And by default, it doesn't have access to any resources. That's why we see that error message on Portal

@paulyuk
Copy link
Author

paulyuk commented Jun 4, 2024

Another assumption - System Assigned MI is used by default. And by default, it doesn't have access to any resources. That's why we see that error message on Portal

Hi, now that the config about has clientId and credential app settings values, I believe the user assigned MI should work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment