Use the TableOutput attribute to write in a Storage Account from your Function by using a Managed Identity
Consider the following scenario in Azure:
- We want to create a new Azure Function triggered by an HTTP request containing some data.
- We want to write this data as new entities into a Storage Table.
- We want to avoid storing any connection string in your code and instead use an Entra ID Managed Identity.
- We want to avoid writing any specific code by using the SDK for accessing the Table.
The TableOutputAttribute
comes to the rescue. Let us see how.
Create a new Resource Group
We are going to create all the resources in this article using the Azure CLI. It is a great way to learn more about the commands compared to simply using the Azure Portal UI.
We will create a new Resource Group to host all the resources we will create and use in this article. The advantage is that once we are done with the exercise, we can delete the Resource Group, and all the contained resources will be deleted too.
Feel free to use the names you want for the created resources. In the following Azure CLI commands I used the <...>
notation to point out the placeholders you will have to change.
az group create \
--name <resource-group-name> \
--location <location>
Create a new Storage Account
You might already have an existing Storage Account to use. If not, you can use the following Azure CLI command to create a new one.
az storage account create \
--name <storage-account-name> \
--resource-group <resource-group-name> \
--kind StorageV2 \
--sku Standard_LRS \
--location <location>
Create a new Function App by using the Azure CLI
We will now create a new Function App which will host the Function we will create next.
In the following command pay attention to:
- We choose the isolated worker mode, which gives us the advantage of a Function that runs in a worker process isolated from the runtime.
- We choose the latest version, which as of the time of writing, is the version 4.
- We define the name of the Storage Account that we created previously. This will create a new application setting with the connection string of the Storage Account in the Function App.
az functionapp create \
--resource-group <resource-group-name> \
--consumption-plan-location <location> \
--runtime dotnet-isolated \
--functions-version 4 \
--name <function-app-name> \
--storage-account <storage-account-name> \
--runtime-version 8.0
Ensure you have the latest version of Azure Functions Core Tools installed
Start a new Visual Studio Code instance, open a Terminal, type func
and press Enter. You should see the version of the installed software. As of September 2024 it should be something higher than 4. In my case it was Core Tools Version: 4.0.5907
.
If you see a lower version, you have to update the software before proceeding. The same applies if the command func
is not available. You will have to install the latest version from here.
Ensure that you have already installed the Azure Functions
extension on your Visual Studio Code. If not, then install this extension also.
Create a new HTTP-triggered Function
Start a new Visual Studio Code instance, create a new project to host your Azure Functions, open a new command prompt and navigate to this folder.
Open the Terminal in Visual Studio Code and create a new Function project by using the following command:
func init <function-app-name> --worker-runtime dotnet-isolated
Navigate into the newly created folder with cd <function-app-name>
and create a new HTTP-triggered Function:
func new --name <function-name> --template "HTTP trigger"
You now have a project ready for programming, which contains files like host.json
, local.settings.json
, etc. Most importantly, a new <function-name>.cs
file was created, which should have the following content:
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Functions;
public class <function-name>
{
private readonly ILogger<<function-name>> _logger;
public <function-name>(ILogger<<function-name>> logger)
{
_logger = logger;
}
[Function("<function-name>")]
public IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
{
_logger.LogInformation("C# HTTP trigger function processed a request.");
return new OkObjectResult("Welcome to Azure Functions!");
}
}
Create a new Storage Entity for storing it into the Storage Table:
using Azure;
namespace Functions;
public class TestEntity : Azure.Data.Tables.ITableEntity
{
public string Text { get; set; }
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public DateTimeOffset? Timestamp { get; set; }
public ETag ETag { get; set; }
}
Update the Function class to create a new object of the TestEntity
. The updated implementation will look like this:
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace Functions;
public class <function-name>
{
private readonly ILogger<<function-name>> _logger;
public <function-name>(ILogger<<function-name>> logger)
{
_logger = logger;
}
[Function("<function-name>")]
public async Task<TestEntity> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
{
_logger.LogInformation("C# HTTP trigger function processed a request.");
return new TestEntity()
{
PartitionKey = "1",
RowKey = Guid.NewGuid().ToString(),
Text = $"Output record with rowkey created at {DateTime.Now}"
};
}
}
The TableOutput attribute
Now, we want the Function to write the returned entities automatically into a Storage Account Table. Add the following NuGet package via Terminal:
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Tables
Add a new using Microsoft.Azure.Functions.Worker.Extensions.Tables
at the top of the Function class.
You can now use the [TableOutput]
attribute. Add the following line after right after the [Function("<function-name>")]
line:
[TableOutput("<storage-table-name>", Connection = "AzureWebJobsStorage")]
I must mention once again that the attribute works only for writing new entities into a CosmoSDB or Azure Storage Table.
The AzureWebJobsStorage
word is a reserved keyword in Azure Functions and currently it points to the connection string of the Storage Account we created before. You can confirm that if you navigate to the Azure Portal, then to your Function App, and open the Application Settings. There is an entry for AzureWebJobsStorage
already stored there.
However, we do not want to deal with connections strings in this example. For that, we will create a new Managed Identity.
Create a System-assigned Managed Identity for the Function App
A System-assigned Managed Identity is bound to the resource it was created for, in our case, the Function App, and when the Function App is deleted, the Managed Identity also gets deleted. You can find more information about Managed Identities in the official Microsoft Documentation.
Run the following Azure CLI command:
az functionapp identity assign --resource-group <resource-group-name> --name <system-managed-identity-name>
Take note of the ObjectID
, you will use it on the next step.
Assign the Storage Account role to the System-assigned Managed Identity
The next step is to allow the created Managed Identity to write in the Storage Account Table. We do that by assigning a new role with the next CLI command.
az role assignment create \
--assignee <object-id> \
--role "Storage Table Data Contributor" \
--scope <storage-account-id>
- The needed role is
Storage Table Data Contributor
. - The
<object-id
is the ObjectID you copied from the previous step. - The
<storage-account-id>
is the complete ID of the Storage Account. You can get this id by running the following CLI command:az storage account show --resource-group <resource-group-name> --name <storage-account-name> --query id
. Copy the resulted string into the command.
Create a new Application Setting for the AzureWebJobsStorage keyword
The Function App is now connected to the Storage Account. We will create a new setting for the property AzureWebJobsStorage
since we are connecting to the Storage Account via the newly created Managed Identity.
There is a convention in Function Apps for using the AzureWebJobsStorage__accountName
key when working with Managed Identities. The value for this key is the name of the Storage Account we created in this article. You can find more information about this feature in the Microsoft documentation.
I know this step looks like magic, but trust me, it works like this.
Run the following CLI command:
az functionapp config appsettings set \
--resource-group <resource-group-name> \
--name <function-app-name> \
--settings AzureWebJobsStorage__accountName=<storage-account-name>
Delete the existing key-value for AzureWebJobsStorage
, we are not going to need it anymore.
az functionapp config appsettings delete \
--resource-group <resource-group-name> \
--name <function-app-name> \
--setting-names AzureWebJobsStorage
Test your Function locally
To test your Functions locally I advise you to use the Azurite Emulator. Install the Azurite
extension in Visual Studio Code.
Next, open the local.settings.json
file and update the AzureWebJobsStorage
value to UseDevelopmentStorage=true
. This will enable you to debug your Function locally.
Deploy your Function to Azure
Use the Azure
extension in Visual Studio Code, connect to your Azure account. Find the newly created Function in the WORKSPACE
area and deploy it to Azure.
Test your Function in the Azure Portal
The final step is to test if the Function can write a new entity into the Storage Account.
Navigate to the Function App in the Azure Portal, find the Function you created, click on it and then click on Test/Run
to start an execution. Your Function will run and add a new row to the Storage Table.
Conclusion
Thats it! In this article we achieved plenty of things:
- Extensively used the Azure CLI to create new resources.
- Utilized the
TableOutput
attribute. - Used the
AzureWebJobsStorage__accountName
application setting.
I hope this article helps you integrate more of your Azure resources into Managed Identities, which is the recommended way for authenticating from Microsoft.