Azure Functions med Aspire

Aspire är en åsiktsstack som förenklar utvecklingen av distribuerade program i molnet. Med integreringen av Aspire med Azure Functions kan du utveckla, felsöka och orkestrera ett Azure Functions .NET-projekt som en del av Aspire-appvärden.

Förutsättningar

Konfigurera utvecklingsmiljön för användning av Azure Functions med Aspire:

Om du använder Visual Studio uppdaterar du till version 17.12 eller senare. Du måste också ha den senaste versionen av Azure Functions-verktygen för Visual Studio. Så här söker du efter uppdateringar:

  1. Gå till Verktygsalternativ>.
  2. Under Projekt och lösningar väljer du Azure Functions.
  3. Välj Sök efter uppdateringar och installera uppdateringar enligt anvisningarna.

Lösningsstruktur

En lösning som använder Azure Functions och Aspire har flera projekt, inklusive ett programvärdprojekt och ett eller flera Functions-projekt.

Appvärdprojektet är startpunkten för ditt program. Den samordnar installationen av komponenterna i ditt program, inklusive Functions-projektet.

Lösningen innehåller vanligtvis också ett standardprojekt för tjänsten . Det här projektet innehåller en uppsättning standardtjänster och konfigurationer som ska användas i olika projekt i ditt program.

Appvärdprojekt

Om du vill konfigurera integreringen kontrollerar du att appvärdprojektet uppfyller följande krav:

  • App-värdprojektet måste referera till Aspire.Hosting.Azure.Functions. Det här paketet definierar den nödvändiga logiken för integreringen.
  • Apptjänstprojektet måste ha en projektreferens för varje funktionsprojekt som du vill inkludera i orkestreringen.
  • I appvärdens AppHost.cs fil måste du inkludera projektet genom att anropa AddAzureFunctionsProject<TProject>() på din IDistributedApplicationBuilder instans. Du använder den här metoden i stället för att använda den AddProject<TProject>() metod som du använder för andra projekttyper i Aspire. Om du använder AddProject<TProject>()kan functions-projektet inte startas korrekt.

I följande exempel visas en minimal AppHost.cs fil för ett apphost-projekt:

var builder = DistributedApplication.CreateBuilder(args);

builder.AddAzureFunctionsProject<Projects.MyFunctionsProject>("MyFunctionsProject");

builder.Build().Run();

Azure Functions-projekt

Om du vill konfigurera integreringen kontrollerar du att Azure Functions-projektet uppfyller följande krav:

  • Functions-projektet måste referera till 2.x-versionerna av Microsoft.Azure.Functions.Worker och Microsoft.Azure.Functions.Worker.Sdk. Du måste också uppdatera alla Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore-referenser till 2.x-versionen.

  • Filen Program.cs måste använda IHostApplicationBuilder-versionen av startprocessen för värdinstansen. Det här kravet innebär att du måste använda FunctionsApplication.CreateBuilder(args).

  • Om lösningen innehåller ett standardprojekt för tjänsten kontrollerar du att functions-projektet är konfigurerat att använda det:

    • Functions-projektet bör innehålla en projektreferens till standardprojektet för tjänsten.
    • Innan du skapar IHostApplicationBuilder i Program.csska du inkludera ett anrop till builder.AddServiceDefaults().

I följande exempel visas en minimal Program.cs fil för ett Functions-projekt som används i Aspire:

using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;

var builder = FunctionsApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.ConfigureFunctionsWebApplication();

builder.Build().Run();

Det här exemplet innehåller inte standardkonfigurationen för Application Insights som visas i många andra Program.cs exempel och i Azure Functions-mallarna. I stället konfigurerar du OpenTelemetry-integrering i Aspire genom att anropa builder.AddServiceDefaults metoden.

Tänk på följande riktlinjer för att få ut mesta möjliga av integreringen:

  • Inkludera inga direkta Application Insights-integreringar i Functions-projektet. Övervakning i Aspire hanteras i stället via dess OpenTelemetry-stöd. Du kan konfigurera Aspire för att exportera data till Azure Monitor via standardprojektet för tjänsten.
  • Definiera inte anpassade appinställningar i local.settings.json filen för Functions-projektet. Den enda inställningen som ska vara i local.settings.json är "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated". Ange alla andra appkonfigurationer via appvärdprojektet.

Anslutningskonfiguration med Aspire

Appvärdprojektet definierar resurser och hjälper dig att skapa anslutningar mellan dem med hjälp av kod. Det här avsnittet visar hur du konfigurerar och anpassar anslutningar som ditt Azure Functions-projekt använder.

Aspire innehåller standardanslutningsbehörigheter som kan hjälpa dig att komma igång. Dessa behörigheter kanske dock inte är lämpliga eller tillräckliga för ditt program.

För scenarier som använder rollbaserad åtkomstkontroll i Azure (RBAC) kan du anpassa behörigheter genom att anropa WithRoleAssignments() metoden för projektresursen. När du anropar WithRoleAssignments()tas alla standardrolltilldelningar bort och du måste uttryckligen definiera de fullständiga rolltilldelningar som du vill använda. Om du hostar din applikation på Azure Container Apps WithRoleAssignments() måste du också anropa AddAzureContainerAppEnvironment()DistributedApplicationBuilder.

Azure Functions-värdlagring

Azure Functions kräver en värdlagringsanslutning (AzureWebJobsStorage) för flera av dess kärnbeteenden. När du anropar AddAzureFunctionsProject<TProject>() i appvärdprojektet skapas en AzureWebJobsStorage anslutning som standard och tillhandahålls till Functions-projektet. Den här standardanslutningen använder Azure Storage-emulatorn för lokala utvecklingskörningar och etablerar automatiskt ett lagringskonto när det distribueras. Om du vill ha mer kontroll kan du ersätta den här anslutningen genom att anropa .WithHostStorage() functions-projektresursen.

Standardbehörigheterna som Aspire anger för värdlagringsanslutningen beror på om du anropar WithHostStorage() eller inte. Genom att lägga till WithHostStorage() tas en tilldelning av rollen lagringskontodeltagare bort. I följande tabell visas de standardbehörigheter som Aspire anger för värdlagringsanslutningen:

Värdlagringsanslutning Standardroller
Inget anrop till WithHostStorage() Storage Blob Data-deltagare,
Lagringsködatadeltagare,
Storage Table Data-medverkare,
Lagringskontomedverkare
Kall WithHostStorage() Storage Blob Data-deltagare,
Lagringsködatadeltagare,
Storage Table Data-deltagare

I följande exempel visas en minimal AppHost.cs fil för ett programvärdprojekt som ersätter värdlagringen och anger en rolltilldelning:

using Azure.Provisioning.Storage;

var builder = DistributedApplication.CreateBuilder(args);

builder.AddAzureContainerAppEnvironment("myEnv");

var myHostStorage = builder.AddAzureStorage("myHostStorage");

builder.AddAzureFunctionsProject<Projects.MyFunctionsProject>("MyFunctionsProject")
    .WithHostStorage(myHostStorage)
    .WithRoleAssignments(myHostStorage, StorageBuiltInRole.StorageBlobDataOwner);

builder.Build().Run();

Anmärkning

Ägare av lagringsblobdata är den roll som vi rekommenderar för de grundläggande behoven för anslutningen till värdlagringen. Din app kan stöta på problem om anslutningen till blobtjänsten bara har standardvärdet Aspire för Storage Blob Data Contributor.

För produktionsscenarier inkluderar du anrop till både WithHostStorage() och WithRoleAssignments(). Du kan sedan ange den här rollen explicit, tillsammans med andra som du behöver.

Utlösar- och bindningsanslutningar

Dina utlösare och bindningar hänvisar till anslutningar med namn. Följande Aspire-integreringar tillhandahåller dessa anslutningar via ett anrop till WithReference() på projektresursen:

Aspire-integrering Standardroller
Azure Blob Storage Storage Blob Data-deltagare,
Lagringsködatadeltagare,
Storage Table Data-deltagare
Azure Queue Storage Storage Blob Data-deltagare,
Lagringsködatadeltagare,
Storage Table Data-deltagare
Azure Event Hubs Azure Event Hubs-dataägare
Azure Service Bus Azure Service Bus-dataägare

I följande exempel visas en minimal AppHost.cs fil för ett programvärdprojekt som konfigurerar en köutlösare. I det här exemplet har motsvarande köutlösare egenskapen Connection inställd på MyQueueTriggerConnection, så anropet till WithReference() anger namnet.

var builder = DistributedApplication.CreateBuilder(args);

var myAppStorage = builder.AddAzureStorage("myAppStorage").RunAsEmulator();
var queues = myAppStorage.AddQueues("queues");

builder.AddAzureFunctionsProject<Projects.MyFunctionsProject>("MyFunctionsProject")
    .WithReference(queues, "MyQueueTriggerConnection");

builder.Build().Run();

För andra integreringar gör anrop till WithReference att ställa in konfigurationen på ett annat sätt. De gör konfigurationen tillgänglig för Aspire-klientintegreringar, men inte för utlösare och bindningar. För dessa integreringar anropar du WithEnvironment() för att skicka anslutningsinformationen för utlösaren eller bindningen för att lösa.

I följande exempel visas hur du anger miljövariabeln MyBindingConnection för en resurs som exponerar ett anslutningsstränguttryck:

builder.AddAzureFunctionsProject<Projects.MyFunctionsProject>("MyFunctionsProject")
    .WithEnvironment("MyBindingConnection", otherIntegration.Resource.ConnectionStringExpression);

Om du vill att både Aspire-klientintegreringar och systemet med utlösare och bindningar ska använda en anslutning kan du konfigurera både WithReference() och WithEnvironment().

För vissa resurser kan strukturen för en anslutning skilja sig från när du kör den lokalt och när du publicerar den till Azure. I föregående exempel otherIntegration kan vara en resurs som körs som en emulator, så ConnectionStringExpression skulle returnera en emulatoranslutningssträng. Men när resursen publiceras kan Aspire konfigurera en identitetsbaserad anslutning och ConnectionStringExpression returnera tjänstens URI. I det här fallet kan du behöva ange ett annat miljövariabelnamn för att konfigurera identitetsbaserade anslutningar för Azure Functions.

I följande exempel används builder.ExecutionContext.IsPublishMode för att villkorligt lägga till det nödvändiga suffixet:

builder.AddAzureFunctionsProject<Projects.MyFunctionsProject>("MyFunctionsProject")
    .WithEnvironment("MyBindingConnection" + (builder.ExecutionContext.IsPublishMode ? "__serviceUri" : ""), otherIntegration.Resource.ConnectionStringExpression);

Mer information om de anslutningsformat som varje bindning stöder och de behörigheter som dessa format kräver finns i bindningens referenssidor.

Värd för applikationen

Aspire har stöd för två olika sätt att vara värd för ditt Functions-projekt i Azure:

I båda fallen distribueras projektet som en container. Aspire tar hand om att skapa containeravbildningen åt dig och push-överföra den till Azure Container Registry.

Publicera som en containerapp

När du publicerar ett Aspire-projekt till Azure distribueras det som standard till Azure Container Apps. Systemet konfigurerar skalningsregler för ditt Functions-projekt med hjälp av KEDA. När du använder Azure Container Apps krävs ytterligare installation för funktionsnycklar. Mer information finns i Åtkomstnycklar i Azure Container Apps .

Åtkomstnycklar i Azure Container Apps

Flera Azure Functions-scenarier använder åtkomstnycklar för att ge en grundläggande åtgärd mot oönskad åtkomst. Till exempel kräver HTTP-utlösarfunktioner som standard att en åtkomstnyckel anropas, även om det här kravet kan inaktiveras med hjälp av AuthLevel egenskapen. Se Arbeta med åtkomstnycklar i Azure Functions för scenarier som kan kräva en nyckel.

När du distribuerar ett Functions-projekt med Aspire till Azure Container Apps skapar eller hanterar systemet inte åtkomstnycklar automatiskt. Om du behöver använda åtkomstnycklar kan du hantera dem som en del av appvärdkonfigurationen. Det här avsnittet visar hur du skapar en tilläggsmetod som du kan anropa från appvärdens Program.cs fil för att skapa och hantera åtkomstnycklar. Den här metoden använder Azure Key Vault för att lagra nycklarna och monterar dem i containerappen som hemligheter.

Anmärkning

Beteendet här förlitar sig på den ContainerApps hemliga providern, som endast är tillgänglig från och med Functions-värdversionen 4.1044.0. Den här versionen är ännu inte tillgänglig i alla regioner, och tills det är, när du publicerar ditt Aspire-projekt, kanske basavbildningen som används för Functions-projektet inte innehåller de nödvändiga ändringarna.

De här stegen kräver Bicep-version 0.38.3 eller senare. Du kan kontrollera Bicep-versionen genom att köra bicep --version från en kommandotolk. Om du har Installerat Azure CLI kan du använda az bicep upgrade för att snabbt uppdatera Bicep till den senaste versionen.

Lägg till följande NuGet-paket i appvärdprojektet:

Skapa en ny klass i appvärdprojektet och inkludera följande kod:

using Aspire.Hosting.Azure;
using Azure.Provisioning.AppContainers;

namespace Aspire.Hosting;

internal static class Extensions
{
    private record SecretMapping(string OriginalName, IAzureKeyVaultSecretReference Reference);

    public static IResourceBuilder<T> PublishWithContainerAppSecrets<T>(
        this IResourceBuilder<T> builder,
        IResourceBuilder<AzureKeyVaultResource>? keyVault = null,
        string[]? hostKeyNames = null,
        string[]? systemKeyExtensionNames = null)
        where T : AzureFunctionsProjectResource
    {
        if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
        {
            return builder;
        }

        keyVault ??= builder.ApplicationBuilder.AddAzureKeyVault("functions-keys");

        var hostKeysToAdd = (hostKeyNames ?? []).Append("default").Select(k => $"host-function-{k}");
        var systemKeysToAdd = systemKeyExtensionNames?.Select(k => $"host-systemKey-{k}_extension") ?? [];
        var secrets = hostKeysToAdd.Union(systemKeysToAdd)
            .Select(secretName => new SecretMapping(
                secretName,
                CreateSecretIfNotExists(builder.ApplicationBuilder, keyVault, secretName.Replace("_", "-"))
            )).ToList();

        return builder
            .WithReference(keyVault)
            .WithEnvironment("AzureWebJobsSecretStorageType", "ContainerApps")
            .PublishAsAzureContainerApp((infra, app) => ConfigureFunctionsContainerApp(infra, app, builder.Resource, secrets));
    }

    private static void ConfigureFunctionsContainerApp(
        AzureResourceInfrastructure infrastructure, 
        ContainerApp containerApp, 
        IResource resource, 
        List<SecretMapping> secrets)
    {
        const string volumeName = "functions-keys";
        const string mountPath = "/run/secrets/functions-keys";

        var appIdentityAnnotation = resource.Annotations.OfType<AppIdentityAnnotation>().Last();
        var containerAppIdentityId = appIdentityAnnotation.IdentityResource.Id.AsProvisioningParameter(infrastructure);

        var containerAppSecretsVolume = new ContainerAppVolume
        {
            Name = volumeName,
            StorageType = ContainerAppStorageType.Secret
        };

        foreach (var mapping in secrets)
        {
            var secret = mapping.Reference.AsKeyVaultSecret(infrastructure);

            containerApp.Configuration.Secrets.Add(new ContainerAppWritableSecret()
            {
                Name = mapping.Reference.SecretName.ToLowerInvariant(),
                KeyVaultUri = secret.Properties.SecretUri,
                Identity = containerAppIdentityId
            });

            containerAppSecretsVolume.Secrets.Add(new SecretVolumeItem
            {
                Path = mapping.OriginalName.Replace("-", "."),
                SecretRef = mapping.Reference.SecretName.ToLowerInvariant()
            });
        }

        containerApp.Template.Containers[0].Value!.VolumeMounts.Add(new ContainerAppVolumeMount
        {
            VolumeName = volumeName,
            MountPath = mountPath
        });
        containerApp.Template.Volumes.Add(containerAppSecretsVolume);
    }

    public static IAzureKeyVaultSecretReference CreateSecretIfNotExists(
        IDistributedApplicationBuilder builder,
        IResourceBuilder<AzureKeyVaultResource> keyVault,
        string secretName)
    {
        var secretParameter = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"param-{secretName}", special: false);
        builder.AddBicepTemplateString($"key-vault-key-{secretName}", """
                param location string = resourceGroup().location
                param keyVaultName string
                param secretName string
                @secure()
                param secretValue string    

                // Reference the existing Key Vault
                resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
                  name: keyVaultName
                }

                // Deploy the secret only if it does not already exist
                @onlyIfNotExists()
                resource newSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
                  parent: keyVault
                  name: secretName
                  properties: {
                      value: secretValue
                  }
                }
                """)
            .WithParameter("keyVaultName", keyVault.GetOutput("name"))
            .WithParameter("secretName", secretName)
            .WithParameter("secretValue", secretParameter);

        return keyVault.GetSecret(secretName);
    }
}

Du kan sedan använda den här metoden i appvärdens Program.cs fil:

builder.AddAzureFunctionsProject<Projects.MyFunctionsProject>("MyFunctionsProject")
       .WithHostStorage(storage)
       .WithExternalHttpEndpoints()
       .PublishWithContainerAppSecrets(systemKeyExtensionNames: ["mcp"]);

I det här exemplet används ett standardnyckelvalv som skapats av tilläggsmetoden. Det resulterar i en standardnyckel och en systemnyckel för användning med tillägget Model Context Protocol.

Om du vill använda dessa nycklar från klienter måste du hämta dem från nyckelvalvet.

Publicera som en funktionsapp

Anmärkning

Publicering som en funktionsapp kräver Aspire Azure App Service-integrering, som för närvarande är i förhandsversion.

Du kan konfigurera Aspire att distribuera till en funktionsapp med hjälp av Aspire Azure App Service-integreringen. Eftersom Aspire publicerar Functions-projektet som en container måste värdplanen för funktionsappen ha stöd för distribution av containerbaserade program.

Följ dessa steg för att publicera ditt Aspire Functions-projekt som en funktionsapp:

  1. Lägg till en referens till NuGet-paketet Aspire.Hosting.Azure.AppService i appvärdprojektet.
  2. I AppHost.cs-filen anropar AddAzureAppServiceEnvironment() på din IDistributedApplicationBuilder-instans för att skapa en App Service-plan. Observera att trots namnet etablerar detta inte en App Service Environment-resurs.
  3. Anropa i Functions-projektresursen .WithExternalHttpEndpoints(). Detta krävs för distribution med Aspire Azure App Service-integreringen.
  4. På Functions-projektresursen anropar du .PublishAsAzureAppServiceWebsite((infra, app) => app.Kind = "functionapp,linux") för att publicera projektet på planen.

Viktigt!

Kontrollera att du anger egenskapen app.Kind till "functionapp,linux". Den här inställningen säkerställer att resursen skapas som en funktionsapp, vilket påverkar funktioner för att arbeta med ditt program.

I följande exempel visas en minimal AppHost.cs fil för ett programvärdprojekt som publicerar ett Functions-projekt som en funktionsapp:

var builder = DistributedApplication.CreateBuilder(args);
builder.AddAzureAppServiceEnvironment("functions-env");
builder.AddAzureFunctionsProject<Projects.MyFunctionsProject>("MyFunctionsProject")
    .WithExternalHttpEndpoints()
    .PublishAsAzureAppServiceWebsite((infra, app) => app.Kind = "functionapp,linux");

Den här konfigurationen skapar en Premium V3-plan. När du använder en dedikerad App Service-plan-SKU är skalning inte händelsebaserad. I stället hanteras skalning via App Service-planinställningarna.

Överväganden och metodtips

Tänk på följande när du utvärderar integreringen av Azure Functions med Aspire:

  • Konfigurationen av utlösare och bindningar via Aspire är för närvarande begränsad till specifika integreringar. Mer information finns i Anslutningskonfiguration med Aspire i den här artikeln.

  • Funktionsprojektets Program.cs fil bör använda IHostApplicationBuilder versionen av uppstart av värdinstansen. IHostApplicationBuilder gör att du kan anropa builder.AddServiceDefaults() för att lägga till Standardinställningar för Aspire-tjänsten i ditt Functions-projekt.

  • Aspire använder OpenTelemetry för övervakning. Du kan konfigurera Aspire för att exportera data till Azure Monitor via standardprojektet för tjänsten.

    I många andra Azure Functions-kontexter kan du inkludera direkt integrering med Application Insights genom att registrera arbetstjänsten. Vi rekommenderar inte den här typen av integrering i Aspire. Det kan leda till körningsfel med version 2.22.0 av Microsoft.ApplicationInsights.WorkerService, men version 2.23.0 åtgärdar det här problemet. När du använder Aspire tar du bort alla direkta Application Insights-integreringar från ditt Functions-projekt.

  • För Functions-projekt som har registrerats i en Aspire-orkestrering bör merparten av programkonfigurationen komma från aspireappens värdprojekt. Undvik att ange saker i local.settings.json, förutom inställningen FUNCTIONS_WORKER_RUNTIME . Om du anger samma miljövariabel i local.settings.json och Aspire använder systemet Aspire-versionen.

  • Konfigurera inte Azure Storage-emulatorn för några anslutningar i local.settings.json. Många Functions-startmallar innehåller emulatorn som standard för AzureWebJobsStorage. Emulatorkonfigurationen kan dock uppmana vissa utvecklarverktyg att starta en version av emulatorn som kan vara i konflikt med den version som Aspire använder.