Use the TableOutput attribute to write in a Storage Account from your Function by using a Managed Identity

Consider the following scenario in Azure:

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:

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>

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:

I hope this article helps you integrate more of your Azure resources into Managed Identities, which is the recommended way for authenticating from Microsoft.

comments powered by Disqus