Search code examples
azureazure-functionsazure-managed-identityazure-storage-queues

Azure Managed ID: how to convert azure function from connection string to managed id


I have an Azure function that looks like this:

   [FunctionName("CreateWidgetWorkspace")]
    public async Task<IActionResult> CreateWidgetWorkspace(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "widget/workspaces")] HttpRequest req,
         [Queue("widgetworkspaces"), StorageAccount("WidgetStorageQueue")] ICollector<string> messageQueue,
        ILogger log)
    {           
       
        WorkspaceResponse response = new WorkspaceResponse();
        var content = await new StreamReader(req.Body).ReadToEndAsync();
        log.LogInformation($"Received following payload: {content}");

        var workspaceRequest = JsonConvert.DeserializeObject<Workspace>(content);
        if (workspaceRequest.name != null){     
                messageQueue.Add(JsonConvert.SerializeObject(workspaceRequest));                                                 
        } 
        else {
            response.status = "Error: Invalid Request";
            response.requestId=null;
        }
        return new OkObjectResult(JsonConvert.SerializeObject(response));  
    }

Everythings works well - the connection string is defined in my local.settings.json file like this:

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "WidgetStorageQueue": 

"DefaultEndpointsProtocol=https;AccountName=accountname;AccountKey=asdf+asdf+AStRrziLg==" } }

But now i've created a managed Identity and it has been assigned "Contributor" role on all resources inside the resource group. So I need to refactor this code to no longer use the connection string in local.settings / environment variables. But to use the managed id. Can you point me to an article or video that would put me on the right path? I actually prefer not to Azure key vault if possible.

Thanks.

EDIT 1

I've added the 2 packages referenced in the answer. This is what I have in my csproj file:

  <ItemGroup>
    <PackageReference Include="Azure.Data.Tables" Version="12.4.0" />
    <PackageReference Include="Azure.Storage.Queues" Version="12.10.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
    <PackageReference Include="Microsoft.Azure.Webjobs.Extensions.ServiceBus" Version="5.2.0" />
    <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage.Queues" Version="5.0.1" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.0.1" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
    </None>
  </ItemGroup>

And this is what my local.settings.json file looks like:

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "dotnet","WidgetStorageQueue__queueServiceUri":"https://mystorageaccountname.queue.core.windows.net"
  },
  "ConnectionStrings": {}
}

But I'm getting an error :

2022-05-05T19:30:00.774Z] Executed 'CreateWidgetWorkspace' (Failed, Id=asdf-a22b-asdf-asdf-asdf, Duration=6356ms)
[2022-05-05T19:30:00.777Z] System.Private.CoreLib: Exception while executing function: CreateWidgetWorkspace. Azure.Storage.Queues: Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
RequestId:asdf-8003-asdf-asdf-asdf
Time:2022-05-05T19:30:00.7494033Z
[2022-05-05T19:30:00.781Z] Status: 403 (Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.)
[2022-05-05T19:30:00.782Z] ErrorCode: AuthenticationFailed
[2022-05-05T19:30:00.784Z] 
[2022-05-05T19:30:00.785Z] Additional Information:
[2022-05-05T19:30:00.788Z] AuthenticationErrorDetail: Issuer validation failed. Issuer did not match.
[2022-05-05T19:30:00.790Z] 
[2022-05-05T19:30:00.791Z] Content:
[2022-05-05T19:30:00.793Z] <?xml version="1.0" encoding="utf-8"?><Error><Code>AuthenticationFailed</Code><Message>Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
RequestId:asdf-8003-asdf-asdf-asdf
Time:2022-05-05T19:30:00.7494033Z</Message><AuthenticationErrorDetail>Issuer validation failed. Issuer did not match.</AuthenticationErrorDetail></Error>      
[2022-05-05T19:30:00.795Z] 
[2022-05-05T19:30:00.796Z] Headers:
[2022-05-05T19:30:00.797Z] Server: Microsoft-HTTPAPI/2.0
[2022-05-05T19:30:00.801Z] x-ms-request-id: asdf-asdf-asdf-asdf-60671b000000
[2022-05-05T19:30:00.802Z] x-ms-error-code: AuthenticationFailed
[2022-05-05T19:30:00.809Z] Date: Thu, 05 May 2022 19:29:59 GMT
[2022-05-05T19:30:00.810Z] Content-Length: 422
[2022-05-05T19:30:00.811Z] Content-Type: application/xml
[2022-05-05T19:30:00.812Z] .

Here's my Azure Resource Group:

enter image description here

Here's the function app - you can see I have assigned a user-assigned managed identity to it:

enter image description here

And here are the RBAC roles assigned my managed identity: enter image description here

Questions

Based on my limited knowledge / reading, it feels like I should install Azure.Identity and create some sort of DefaultAzureCredential? https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme#specifying-a-user-assigned-managed-identity-with-the-defaultazurecredential

EDIT 2

The changes suggested in the answer basically work. To clarify, the setting in local.setting.json that actually works is this:

"[nameofConnectioninC#Method]__serviceUri":"https://[nameOfStorageAccount].queue.core.windows.net/"

This fails when you local debug, but when you publish everything, upstream tests work.


Solution

  • To use Identity-based connections with Queue Storage, you will need to:

    1. Update you application to use these Nuget Packages: Azure.Storage.Queues and Microsoft.Azure.WebJobs.Extensions.Storage.Queues.

    2. Create an app settings called <CONNECTION_NAME_PREFIX>__serviceUri:

      The data plane URI of the queue service to which you are connecting, using the HTTPS scheme.
      https://<storage_account_name>.queue.core.windows.net

      So in your case you need to create a setting called WidgetStorageQueue__serviceUri

    3. Grant permission to your function app identity to access queue storage. If you just need to send message to a queue, you could use the Storage Queue Data Message Sender role.

    I've created a small function app that reproduces this use case.

    csproj file:

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <AzureFunctionsVersion>v4</AzureFunctionsVersion>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="Azure.Storage.Queues" Version="12.9.0" />
        <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage.Queues" Version="5.0.0" />
        <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.1.0" />
      </ItemGroup>
      <ItemGroup>
        <None Update="host.json">
          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        </None>
        <None Update="local.settings.json">
          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
          <CopyToPublishDirectory>Never</CopyToPublishDirectory>
        </None>
      </ItemGroup>
    </Project>
    
    

    function file

    using System.IO;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Azure.WebJobs;
    using Microsoft.Azure.WebJobs.Extensions.Http;
    using Microsoft.AspNetCore.Http;
    using Microsoft.Extensions.Logging;
    
    namespace FunctionApp1
    {
        public static class Function1
        {
            [FunctionName("Function1")]
            public static async Task<IActionResult> Run(
                [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
                [Queue("widgetworkspaces"), StorageAccount("WidgetStorageQueue")] ICollector<string> queueCollector,
                ILogger log)
            {
                string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
                queueCollector.Add(requestBody);
                return new OkObjectResult(requestBody);
            }
        }
    }
    
    

    settings file

    {
      "IsEncrypted": false,
      "Values": {
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet",
        "WidgetStorageQueue__queueServiceUri": "https://<storage_account_name>.queue.core.windows.net"
      }
    }