Usługa Azure Functions z aspirem

Aspire to łańcuch narzędzi do budowania, uruchamiania, debugowania i wdrażania aplikacji rozproszonych. Integracja z Aspire Azure Functions pozwala rozwijać, debugować i orkiestrować projekt Azure Functions jako część Aspire AppHost. Przykłady .NET w tym artykule wykorzystują model izolowanego pracownika.

Wymagania wstępne

Skonfiguruj środowisko programistyczne na potrzeby korzystania z usługi Azure Functions z aplikacją Aspire:

Jeśli korzystasz z Visual Studio, zainstaluj najnowsze aktualizacje narzędzi Visual Studio i Azure Functions:

  1. Przejdź do Narzędzia>Opcje.
  2. W obszarze Projekty i rozwiązania wybierz pozycję Azure Functions.
  3. Wybierz pozycję Sprawdź dostępność aktualizacji i zainstaluj aktualizacje zgodnie z monitem.

Więcej informacji o pakiecie integracyjnym i obsługiwanych API AppHost znajdziesz w artykule Ustaw Azure Functions w AppHost.

Struktura rozwiązania

Rozwiązanie korzystające z Azure Functions i Aspire składa się z wielu projektów, w tym AppHost oraz jednego lub więcej projektów Functions.

AppHost jest punktem wejścia do Twojej aplikacji. Organizuje ona konfigurację składników aplikacji, w tym projekt usługi Functions.

Rozwiązanie zwykle zawiera również projekt ustawień domyślnych usługi. Ten projekt udostępnia zestaw domyślnych usług i konfiguracji, które mają być używane w projektach w aplikacji.

Projekt AppHost

Aby pomyślnie skonfigurować integrację, upewnij się, że projekt AppHost spełnia następujące wymagania:

  • AppHost odwołuje się do Aspire.Hosting.Azure.Functions. Ten pakiet definiuje integrację.
  • AppHost języka C# odwołuje się do projektu Functions i wywołuje AddAzureFunctionsProject<TProject>() lub wywołuje AddAzureFunctionsProject(name, projectPath) ze ścieżką do pliku projektu. Hosty aplikacji TypeScript AppHost używają formatu addAzureFunctionsProject project-path.
  • Użyj AddAzureFunctionsProject zamiast AddProject. Projekt Functions dodany za pomocą AddProject nie może zostać poprawnie uruchomiony.

Poniższy przykład przedstawia minimalny plik AppHost.cs dla projektu AppHost w języku C#:

var builder = DistributedApplication.CreateBuilder(args);

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

builder.Build().Run();

Projekt usługi Azure Functions

Aby pomyślnie skonfigurować integrację, upewnij się, że projekt usługi Azure Functions spełnia następujące wymagania:

W poniższym przykładzie pokazano minimalny plik Program.cs dla projektu Functions używanego w Aspire.

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

var builder = FunctionsApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.ConfigureFunctionsWebApplication();

builder.Build().Run();

Ten przykład nie zawiera domyślnej konfiguracji usługi Application Insights, która jest wyświetlana w wielu innych Program.cs przykładach i w szablonach usługi Azure Functions. Zamiast tego konfigurujesz integrację OpenTelemetry w Aspire, wywołując metodę builder.AddServiceDefaults().

Aby jak najlepiej wykorzystać integrację, należy wziąć pod uwagę następujące wytyczne:

  • Nie uwzględniaj żadnych bezpośrednich integracji usługi Application Insights w projekcie usługi Functions. Monitorowanie w Aspire jest obsługiwane za pośrednictwem OpenTelemetry. Możesz skonfigurować Aspire do eksportowania danych do Azure Monitor poprzez projekt domyślnych ustawień usługi.
  • Gdy Aspire uruchamia projekt Functions, należy preferować ustawienia przekazywane przez AppHost. Możesz zachować odpowiednie ustawienia w local.settings.json, aby uruchamiać projekt niezależnie za pomocą func start; zmienne środowiskowe wstrzykiwane przez Aspire zastąpią je.

Konfiguracja połączenia z aplikacją Aspire

AppHost definiuje zasoby i pomaga tworzyć połączenia między nimi za pomocą kodu. W tej sekcji przedstawiono sposób konfigurowania i dostosowywania połączeń używanych przez projekt usługi Azure Functions.

Aspire zawiera domyślne uprawnienia do połączeń, które mogą pomóc w rozpoczęciu pracy. Jednak te uprawnienia mogą nie być odpowiednie lub wystarczające dla aplikacji.

W przypadku scenariuszy korzystających z kontroli dostępu opartej na rolach (RBAC) platformy Azure można dostosować uprawnienia, wywołując metodę WithRoleAssignments() w zasobie projektu. Po wywołaniu WithRoleAssignments() wszystkie domyślne przypisania ról zostaną usunięte i należy jawnie zdefiniować wszystkie przypisania ról, które chcesz. Jeśli hostujesz aplikację w usłudze Azure Container Apps, użycie WithRoleAssignments() również wymaga wywołania AddAzureContainerAppEnvironment() na DistributedApplicationBuilder.

Pamięć hosta dla Azure Functions

Usługa Azure Functions wymaga połączenia magazynu hosta (AzureWebJobsStorage) dla kilku podstawowych zachowań. Gdy wywołujesz AddAzureFunctionsProject<TProject>() w swoim AppHost, domyślnie tworzysz połączenie AzureWebJobsStorage i przekazujesz je do projektu Functions. To domyślne połączenie korzysta z emulatora Azure Storage do lokalnych uruchomieni programistycznych i automatycznie alokuje konto pamięci masowej podczas wdrożenia. Aby uzyskać większą kontrolę, zastąp to połączenie, wywołując .WithHostStorage() na zasobie projektu Functions.

Domyślne uprawnienia, które Aspire ustawia dla połączenia z pamięcią hosta, zależą od tego, czy wywołasz WithHostStorage() , czy nie. Dodanie WithHostStorage() usuwa przypisanie uczestnika konta magazynu. Aspire ustawia domyślne uprawnienia dla połączenia pamięci masowej hosta w poniższej tabeli.

Połączenie pamięci masowej hosta Role domyślne
Brak połączenia z WithHostStorage() Współautor danych obiektu blob usługi Storage,
Współautor danych kolejki usługi Storage,
Kontrybutor danych tabeli Storage
Współpracownik konta magazynowego
Wywołanie WithHostStorage() Współautor danych obiektu blob usługi Storage,
Współautor danych kolejki usługi Storage,
Współautor danych tabeli usługi Storage

Poniższy przykład przedstawia minimalny plik AppHost.cs, który zastępuje magazyn hosta i określa przypisanie roli:

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();

Note

Właściciel danych obiektu blob usługi Storage jest rolą zalecaną dla podstawowych potrzeb połączenia magazynu hosta. Aplikacja może napotkać problemy, jeśli połączenie z usługą blob ma tylko domyślną rolę Aspire Storage Blob Data Contributor.

W przypadku scenariuszy produkcyjnych należy uwzględnić wywołania zarówno do WithHostStorage(), jak i WithRoleAssignments(). Następnie możesz jawnie ustawić tę rolę wraz ze wszystkimi innymi, których potrzebujesz.

Wyzwalanie i wiązanie połączeń

Mechanizmy wyzwalające i powiązania odwołują się do połączeń według ich nazw. Następujące integracje Aspire umożliwiają te połączenia poprzez wywołanie zasobu projektu WithReference():

Integracja z Aspire Role domyślne
Azure Blob Storage Współautor danych obiektu blob usługi Storage,
Współautor danych kolejki usługi Storage,
Współautor danych tabeli usługi Storage
Azure Queue Storage Współautor danych obiektu blob usługi Storage,
Współautor danych kolejki usługi Storage,
Współautor danych tabeli usługi Storage
Azure Event Hubs Właściciel danych usługi Azure Event Hubs
Azure Service Bus Właściciel danych usługi Azure Service Bus

Poniższy przykład pokazuje minimalny AppHost.cs plik konfigurujący wyzwalacz kolejki. W tym przykładzie odpowiedni wyzwalacz kolejki ma właściwość Connection ustawioną na MyQueueTriggerConnection, więc wywołanie WithReference() określa nazwę.

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();

W przypadku innych integracji wywołania do WithReference ustawiają konfigurację w inny sposób. Udostępniają konfigurację dla integracji klienta Aspire, ale nie dla wyzwalaczy i powiązań. W przypadku tych integracji wywołaj metodę WithEnvironment() , aby przekazać informacje o połączeniu wyzwalacza lub powiązania w celu rozwiązania problemu.

W poniższym przykładzie pokazano, jak ustawić zmienną środowiskową MyBindingConnection dla zasobu, który uwidacznia wyrażenie parametrów połączenia:

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

Jeśli chcesz, aby zarówno integracje klientów Aspire, jak i system wyzwalaczy oraz powiązań używać połączenia, możesz skonfigurować zarówno WithReference(), jak i WithEnvironment().

W przypadku niektórych zasobów struktura połączenia może się różnić w przypadku uruchamiania go lokalnie i podczas publikowania go na platformie Azure. W poprzednim przykładzie otherIntegration może to być zasób, który działa jako emulator, więc ConnectionStringExpression zwraca parametry połączenia emulatora. Jednak po opublikowaniu zasobu Aspire może ustanowić połączenie oparte na tożsamości i ConnectionStringExpression zwróci identyfikator URI usługi. W takim przypadku, aby skonfigurować połączenia oparte na tożsamości dla usługi Azure Functions, może być konieczne podanie innej nazwy zmiennej środowiskowej.

W poniższym przykładzie użyto builder.ExecutionContext.IsPublishMode do warunkowego dodania niezbędnego sufiksu.

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

Aby uzyskać szczegółowe informacje na temat formatów połączeń, które obsługuje każde powiązanie, oraz uprawnienia wymagane przez te formaty, zapoznaj się ze stronami referencyjnymi powiązania.

Aby uzyskać więcej informacji o tym, jak kod Functions odczytuje wartości wstrzyknięte przez WithReference, zobacz konfigurację wykonawczą Azure Functions.

Hostowanie aplikacji

Aspire wspiera wdrażanie Azure Container Apps dla projektów Functions. Możesz także użyć oddzielnej integracji App Service w wersji zapoznawczej, aby określić jako docelową aplikację funkcji obsługującą kontenery:

W obu przypadkach projekt jest wdrażany jako kontener. Aspire zajmuje się tworzeniem obrazu kontenera i wypychaniem go do Azure Container Registry.

Wdrażaj jako aplikację kontenerową

Gdy twój AppHost jest przeznaczony dla Azure Container Apps, Aspire konfiguruje reguły skalowania dla projektu Functions przy użyciu KEDA. Korzystając z Azure Container Apps, musisz przeprowadzić dodatkową konfigurację kluczy funkcji. Więcej informacji można znaleźć w artykule Klucze dostępu w Azure Container Apps.

Wdroż skonfigurowany AppHost, uruchamiając aspire deploy. Więcej informacji można znaleźć w Deploy to Azure Container Apps oraz aspire deploy.

Klucze dostępu w usłudze Azure Container Apps

Kilka scenariuszy usługi Azure Functions używa kluczy dostępu, aby zapewnić podstawowe środki zaradcze przed niepożądanym dostępem. Na przykład funkcje wyzwalacza HTTP domyślnie wymagają wywołania klucza dostępu, chociaż to wymaganie można wyłączyć przy użyciu AuthLevel właściwości . Zobacz Praca z kluczami dostępu w usłudze Azure Functions, aby zapoznać się ze scenariuszami, które mogą wymagać klucza.

Gdy wdrażasz projekt Functions za pomocą Aspire do Azure Container Apps, system nie tworzy automatycznie ani nie zarządza kluczami dostępu do Functions. Jeśli musisz używać kluczy dostępu, możesz nimi zarządzać w ramach konfiguracji AppHost. Ta sekcja pokazuje, jak stworzyć metodę rozszerzenia, którą możesz wywołać z pliku AppHost, AppHost.cs aby tworzyć i zarządzać kluczami dostępu. To podejście używa usługi Azure Key Vault do przechowywania kluczy i podpinania ich w aplikacji kontenerowej jako sekrety.

Note

To zachowanie opiera się na dostawcy sekretów ContainerApps, który wymaga wersji hosta Functions 4.1044.0 lub nowszej.

Te kroki wymagają wersji Bicep lub nowszej 0.38.3 . Wersję Bicep można sprawdzić, uruchamiając polecenie bicep --version w wierszu polecenia. Jeśli masz zainstalowany interfejs wiersza polecenia platformy Azure, możesz użyć az bicep upgrade polecenia , aby szybko zaktualizować aplikację Bicep do najnowszej wersji.

Dodaj do swojego projektu AppHost następujące pakiety NuGet:

Stwórz nową klasę w swoim projekcie AppHost i dołącz następujący 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);
    }
}

Następnie możesz użyć tej metody w pliku AppHosta AppHost.cs:

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

W tym przykładzie użyto domyślnego magazynu kluczy utworzonego przez metodę rozszerzenia. Skutkuje to kluczem domyślnym i kluczem systemowym dla rozszerzenia Protokołu kontekstu modelu.

Aby używać tych kluczy od klientów, należy pobrać je z magazynu kluczy.

Wdrażaj jako aplikację funkcyjną

Note

Wdrożenie jako aplikacja funkcjonalna wymaga integracji z Aspire Azure App Service, która jest obecnie w fazie podglądu.

Możesz skonfigurować Aspire do wdrażania do aplikacji funkcji za pomocą integracji Aspire z usługą Azure App Service. Ponieważ Aspire wdraża projekt Functions jako kontener, plan hostingu dla Twojej aplikacji Function musi wspierać wdrażanie aplikacji kontenerowych.

Aby wdrożyć swój projekt Aspire Functions jako aplikację funkcyjną, postępuj zgodnie z następującymi krokami:

  1. W katalogu AppHost uruchom aspire add Aspire.Hosting.Azure.AppService, aby dodać pakiet NuGet Aspire.Hosting.Azure.AppService.
  2. W pliku wywołaj AppHost.csAddAzureAppServiceEnvironment()IDistributedApplicationBuilder wystąpienie, aby utworzyć plan usługi App Service. Należy pamiętać, że pomimo nazwy nie zapewnia to zasobu Środowiska Usług Aplikacyjnych (App Service Environment).
  3. W zasobie projektu Functions wywołaj metodę .WithExternalHttpEndpoints(). Jest to wymagane do wdrożenia z integracją Aspire Azure App Service.
  4. W zasobach projektu Functions wywołaj .PublishAsAzureAppServiceWebsite((infra, app) => app.Kind = "functionapp,linux") , aby dostosować ten projekt jako aplikację funkcjonalną w planie.

Important

Upewnij się, że ustawisz właściwość app.Kind na "functionapp,linux". To ustawienie gwarantuje, że zasób jest tworzony jako aplikacja funkcji, co wpływa na doświadczenie w pracy z aplikacją.

Poniższy przykład przedstawia minimalny plik AppHost.cs, który wdraża projekt usługi Functions jako aplikację funkcji:

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

builder.Build().Run();

Ta konfiguracja tworzy plan Premium V3. W przypadku korzystania z dedykowanego planu usługi App Service SKU, skalowanie nie bazuje na zdarzeniach. Zamiast tego skalowanie jest zarządzane za pomocą ustawień planu usługi App Service.

Zagadnienia i najlepsze rozwiązania

Podczas oceniania integracji usługi Azure Functions z aplikacją Aspire należy wziąć pod uwagę następujące kwestie:

  • Konfiguracja wyzwalacza i wiązania za pośrednictwem platformy Aspire jest obecnie ograniczona do określonych integracji. Aby uzyskać szczegółowe informacje, zobacz Konfiguracja połączenia z aplikacją Aspire w tym artykule.

  • Plik projektu Program.cs funkcji powinien używać IHostApplicationBuilder wersji uruchamiania wystąpienia hosta. Korzystając z IHostApplicationBuilder, możesz użyć builder.AddServiceDefaults(), aby dodać domyślne ustawienia usług Aspire do projektu Functions.

  • Aspire używa OpenTelemetry do monitorowania. Możesz skonfigurować Aspire do eksportowania danych do Azure Monitor poprzez projekt domyślnych ustawień usługi.

    W wielu innych kontekstach usługi Azure Functions można uwzględnić bezpośrednią integrację z usługą Application Insights, rejestrując usługę roboczą. Nie rejestruj drugiego, bezpośredniego potoku Application Insights, jeśli używasz Aspire Service Defaults.

  • Dla projektów Functions wpisanych do orkiestracji Aspire, AppHost powinien zapewnić większość konfiguracji aplikacji. Możesz użyć local.settings.json do samodzielnego uruchomienia projektu Functions za pomocą func start. Gdy Aspire uruchamia projekt, zmienne środowiskowe wstrzyknięte przez Aspire zastępują wartości o tych samych nazwach w local.settings.json.

  • Unikaj uruchamiania drugiego emulatora Azure Storage dla połączeń, którymi zarządza AppHost. Konkurencyjne instancje emulatorów mogą powodować konflikty portów i pamięci masowej.

Więcej informacji można znaleźć w Azure Functions runtime configuration oraz Aspire telemetry.