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.
- 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 itStorageConnection
) will for now share the same storage account connection.
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.
- In a new terminal, create a new folder for your code, e.g.
cd ~/src
mkdir secure-sample
- Open in VS Code
cd secure-sample
code .
- 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.
- Inspect
local.settings.json
It probably looks like this:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"238a05_STORAGE": "UseDevelopmentStorage=true"
}
}
- 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)
- 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"
}
}
- 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
- run the app
Either press F5 to Run
or in the terminal:
func start
Now there is a working, password free local only template.
- 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/"
},
- change the settings in
local.settings.json
to leverage your Connection name ofStorageConnection
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.
- Grant your interactive signed in Azure account (e.g. [email protected] 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
- 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.
- 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>
-
Once deployed, edit the app settings to match what you had in local settings exactly - names and values.
-
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.
-
Set
blobfunction-identity
as the User-assigned identity for your function in FunctionApp -> Identities -
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
}
]
- restart the function for good measure. and test it!
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