Partage via


Azure Functions avec Aspire

Aspire est une pile avisée qui simplifie le développement d’applications distribuées dans le cloud. L’intégration d’Aspire à Azure Functions vous permet de développer, de déboguer et d’orchestrer un projet .NET Azure Functions dans le cadre de l’hôte d’application Aspire.

Conditions préalables

Configurez votre environnement de développement pour l’utilisation d’Azure Functions avec Aspire :

  • Installez les prérequis d'Aspire.
    • La prise en charge complète de l’intégration d’Azure Functions nécessite Aspire 13.1 ou version ultérieure. Aspire 13.0 inclut également une préversion de Aspire.Hosting.Azure.Functions, qui joue le rôle d’une version finale (RC) pour le déploiement avec prise en charge du déploiement.
  • Installez Azure Functions Core Tools.

Si vous utilisez Visual Studio, mettez à jour vers la version 17.12 ou ultérieure. Vous devez également disposer de la dernière version des outils Azure Functions pour Visual Studio. Pour rechercher les mises à jour :

  1. Accédez à Outils>Options.
  2. Sous Projets et solutions, sélectionnez Azure Functions.
  3. Sélectionnez Rechercher les mises à jour et installez les mises à jour comme indiqué.

Structure de la solution

Une solution qui utilise Azure Functions et Aspire a plusieurs projets, notamment un projet hôte d’application et un ou plusieurs projets Functions.

Le projet hôte d’application est le point d’entrée de votre application. Il orchestre la configuration des composants de votre application, y compris le projet Functions.

La solution inclut généralement un projet de service par défaut . Ce projet fournit un ensemble de services et de configurations par défaut à utiliser dans les projets de votre application.

Projet hôte d’application

Pour configurer correctement l’intégration, vérifiez que le projet hôte d’application répond aux exigences suivantes :

  • Le projet hôte d’application doit référencer Aspire.Hosting.Azure.Functions. Ce package définit la logique nécessaire pour l’intégration.
  • Le projet hôte d’application doit avoir une référence de projet pour chaque projet Functions que vous souhaitez inclure dans l’orchestration.
  • Dans le fichier de l’hôte d’application AppHost.cs, vous devez inclure le projet en appelant AddAzureFunctionsProject<TProject>() dans votre instance IDistributedApplicationBuilder. Vous utilisez cette méthode au lieu d’utiliser la AddProject<TProject>() méthode que vous utilisez pour d’autres types de projet dans Aspire. Si vous utilisez AddProject<TProject>(), le projet Functions ne peut pas démarrer correctement.

L’exemple suivant montre un fichier minimal AppHost.cs pour un projet hôte d’application :

var builder = DistributedApplication.CreateBuilder(args);

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

builder.Build().Run();

Projet Azure Functions

Pour configurer l’intégration, vérifiez que le projet Azure Functions répond aux exigences suivantes :

  • Le projet Functions doit référencer les versions 2.x de Microsoft.Azure.Functions.Worker et Microsoft.Azure.Functions.Worker.Sdk. Vous devez également mettre à jour les références Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore à la version 2.x.

  • Votre Program.cs fichier doit utiliser la IHostApplicationBuilder version du démarrage de l’instance hôte. Cette exigence signifie que vous devez utiliser FunctionsApplication.CreateBuilder(args).

  • Si votre solution inclut un projet par défaut de service, vérifiez que votre projet Functions est configuré pour l’utiliser :

    • Le projet Functions doit inclure une référence de projet au projet par défaut du service.
    • Avant de construire IHostApplicationBuilder dans Program.cs, incluez un appel à builder.AddServiceDefaults().

L’exemple suivant montre un fichier minimal Program.cs pour un projet Functions utilisé dans Aspire :

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

var builder = FunctionsApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.ConfigureFunctionsWebApplication();

builder.Build().Run();

Cet exemple n’inclut pas la configuration Application Insights par défaut qui apparaît dans de nombreux autres Program.cs exemples et dans les modèles Azure Functions. Au lieu de cela, vous configurez l’intégration d’OpenTelemetry dans Aspire en appelant la builder.AddServiceDefaults méthode.

Pour tirer le meilleur parti de l’intégration, tenez compte des instructions suivantes :

  • N’incluez pas d’intégrations Application Insights directes dans le projet Functions. La surveillance dans Aspire est plutôt gérée par le biais de sa prise en charge d’OpenTelemetry. Vous pouvez configurer Aspire pour exporter des données vers Azure Monitor via le projet par défaut du service.
  • Ne définissez pas les paramètres d’application personnalisés dans le local.settings.json fichier du projet Functions. Le seul paramètre qui doit être dans local.settings.json est "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated". Définissez toutes les autres configurations d’application via le projet hôte d’application.

Configuration de connexion avec Aspire

Le projet hôte d’application définit des ressources et vous aide à créer des connexions entre elles à l’aide du code. Cette section montre comment configurer et personnaliser les connexions utilisées par votre projet Azure Functions.

Aspire inclut des autorisations de connexion par défaut qui peuvent vous aider à commencer. Toutefois, ces autorisations peuvent ne pas être appropriées ou suffisantes pour votre application.

Pour les scénarios qui utilisent le contrôle d’accès en fonction du rôle Azure (RBAC), vous pouvez personnaliser les autorisations en appelant la WithRoleAssignments() méthode sur la ressource de projet. Lorsque vous appelez WithRoleAssignments(), toutes les attributions de rôles par défaut sont supprimées et vous devez définir explicitement les attributions de rôles de jeu complet souhaitées. Si vous hébergez votre application sur Azure Container Apps, l’utilisation de WithRoleAssignments() nécessite également que vous appeliez AddAzureContainerAppEnvironment() sur DistributedApplicationBuilder.

Stockage hôte Azure Functions

Azure Functions nécessite une connexion de stockage hôte (AzureWebJobsStorage) pour plusieurs de ses comportements de base. Lorsque vous appelez AddAzureFunctionsProject<TProject>() dans votre projet hôte d'application, une connexion AzureWebJobsStorage est créée par défaut et fournie au projet de Fonctions. Cette connexion par défaut utilise l’émulateur stockage Azure pour les exécutions de développement local et provisionne automatiquement un compte de stockage lorsqu’il est déployé. Pour plus de contrôle, vous pouvez remplacer cette connexion en appelant .WithHostStorage() la ressource de projet Functions.

Les autorisations par défaut qu'Aspire définit pour la connexion de stockage de l'hôte dépendent de si vous appelez WithHostStorage() ou non. L’ajout WithHostStorage() supprime une attribution contributeur de compte de stockage . Le tableau suivant répertorie les autorisations par défaut que Aspire définit pour la connexion de stockage hôte :

Connexion de stockage hôte Rôles par défaut
Aucun appel à WithHostStorage() Contributeur aux données Blob du stockage,
Contributeur aux données en file d’attente du stockage,
Contributeur de données de table de stockage
Contributeur au compte de stockage
Appel de WithHostStorage() Contributeur aux données Blob du stockage,
Contributeur aux données en file d’attente du stockage,
Contributeur aux données de table du stockage

L’exemple suivant montre un fichier minimal AppHost.cs pour un projet hôte d’application qui remplace le stockage hôte et spécifie une attribution de rôle :

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

Remarque

Propriétaire des données blob du stockage est le rôle que nous recommandons pour les besoins de base de la connexion de stockage d’hôte. Votre application peut rencontrer des problèmes si la connexion au service d’objets blob ne dispose que du rôle par défaut Contributeur aux données Blob du stockage dans Aspire.

Pour les scénarios de production, incluez des appels à WithHostStorage() et à WithRoleAssignments(). Vous pouvez ensuite définir ce rôle explicitement, ainsi que les autres dont vous avez besoin.

Connexions de déclencheur et de liaison

Vos déclencheurs et liaisons référencent les connexions par nom. Les intégrations Aspire suivantes fournissent ces connexions via un appel à WithReference() sur la ressource de projet.

Intégration d’Aspire Rôles par défaut
Stockage Blob Azure Contributeur aux données Blob du stockage,
Contributeur aux données en file d’attente du stockage,
Contributeur aux données de table du stockage
Stockage de files d'attente Azure Contributeur aux données Blob du stockage,
Contributeur aux données en file d’attente du stockage,
Contributeur aux données de table du stockage
Azure Event Hubs Propriétaire de données Azure Event Hubs
Azure Service Bus Propriétaire de données Azure Service Bus

L’exemple suivant montre un fichier minimal AppHost.cs pour un projet hôte d’application qui configure un déclencheur de file d’attente. Dans cet exemple, le déclencheur de file d’attente correspondant a sa propriété Connection définie sur MyQueueTriggerConnection, de sorte que l’appel à WithReference() spécifie le nom.

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

Pour d'autres intégrations, les appels à WithReference définissent la configuration d'une manière différente. Ils rendent la configuration disponible pour les intégrations des clients Aspire, mais pas pour les déclencheurs et les liaisons. Pour ces intégrations, appelez WithEnvironment() pour passer les informations de connexion pour le déclencheur ou la liaison à résoudre.

L’exemple suivant montre comment définir la variable MyBindingConnection d’environnement d’une ressource qui expose une expression de chaîne de connexion :

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

Si vous souhaitez que les intégrations clientes Aspire et le système de déclencheurs et de liaisons utilisent une connexion, vous pouvez configurer à la fois WithReference() et WithEnvironment().

Pour certaines ressources, la structure d’une connexion peut être différente entre le moment où vous l’exécutez localement et celui où vous la publiez sur Azure. Dans l’exemple précédent, otherIntegration peut être une ressource qui s’exécute en tant qu’émulateur de sorte que ConnectionStringExpression retourne une chaîne de connexion d’émulateur. Toutefois, lorsque la ressource est publiée, Aspire peut configurer une connexion basée sur l’identité et ConnectionStringExpression retournerait l’URI du service. Dans ce cas, pour configurer des connexions basées sur des identités pour Azure Functions, vous devrez peut-être fournir un autre nom de variable d’environnement.

L’exemple suivant utilise builder.ExecutionContext.IsPublishMode pour ajouter conditionnellement le suffixe nécessaire :

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

Pour plus d’informations sur les formats de connexion pris en charge par chaque liaison et les autorisations requises par ces formats, consultez les pages de référence de la liaison.

Hébergement de l’application

Aspire prend en charge deux façons différentes d’héberger votre projet Functions dans Azure :

Dans les deux cas, votre projet est déployé en tant que conteneur. Aspire s’occupe de la création de l’image conteneur pour vous et de son envoi vers Azure Container Registry.

Publier en tant qu’application conteneur

Par défaut, lorsque vous publiez un projet Aspire sur Azure, il est déployé sur Azure Container Apps. Le système configure des règles de mise à l’échelle pour votre projet Functions à l’aide de KEDA. Lors de l’utilisation d’Azure Container Apps, une configuration supplémentaire est nécessaire pour les clés de fonction. Pour plus d’informations , consultez les clés d’accès sur Azure Container Apps .

Clés d’accès sur Azure Container Apps

Plusieurs scénarios Azure Functions utilisent des clés d’accès pour fournir une atténuation de base contre les accès indésirables. Par exemple, les fonctions de déclencheur HTTP nécessitent par défaut l’appel d’une clé d’accès, bien que cette exigence puisse être désactivée à l’aide de la AuthLevel propriété. Consultez Utiliser des clés d’accès dans Azure Functions pour les scénarios qui peuvent nécessiter une clé.

Lorsque vous déployez un projet Functions à l’aide d’Aspire dans Azure Container Apps, le système ne crée pas ou ne gère pas automatiquement les clés d’accès Functions. Si vous devez utiliser des clés d’accès, vous pouvez les gérer dans le cadre de la configuration de votre hôte d’application. Cette section vous montre comment créer une méthode d’extension que vous pouvez appeler à partir du fichier de votre hôte d’application Program.cs pour créer et gérer des clés d’accès. Cette approche utilise Azure Key Vault pour stocker les clés et les monter dans l’application conteneur en tant que secrets.

Remarque

Le comportement ici s’appuie sur le ContainerApps fournisseur de secrets, qui est disponible uniquement à partir de la version 4.1044.0de l’hôte Functions. Cette version n’est pas encore disponible dans toutes les régions et jusqu’à ce qu’elle soit, lorsque vous publiez votre projet Aspire, l’image de base utilisée pour le projet Functions peut ne pas inclure les modifications nécessaires.

Ces étapes nécessitent la version de Bicep 0.38.3 ou une version ultérieure. Vous pouvez vérifier votre version de Bicep en exécutant bicep --version à partir d'une invite de commandes. Si l’interface de ligne de commande Azure est installée, vous pouvez utiliser az bicep upgrade pour mettre à jour rapidement Bicep vers la dernière version.

Ajoutez les packages NuGet suivants à votre projet hôte d’application :

Créez une classe dans votre projet hôte d’application et incluez le code suivant :

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

Vous pouvez ensuite utiliser cette méthode dans le fichier de l’hôte de Program.cs votre application :

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

Cet exemple utilise un coffre de clés par défaut créé par la méthode d’extension. Elle génère une clé par défaut et une clé système à utiliser avec l’extension Model Context Protocol.

Pour utiliser ces clés à partir de clients, vous devez les récupérer à partir du coffre de clés.

Publier en tant qu’application de fonction

Remarque

La publication en tant qu’application de fonction nécessite l’intégration d’Aspire Azure App Service, actuellement en préversion.

Vous pouvez configurer Aspire pour effectuer un déploiement sur une application de fonction à l’aide de l’intégration d’Aspire Azure App Service. Étant donné que Aspire publie le projet Functions en tant que conteneur, le plan d’hébergement de votre application de fonction doit prendre en charge le déploiement d’applications conteneurisées.

Pour publier votre projet Aspire Functions en tant qu’application de fonction, procédez comme suit :

  1. Ajoutez une référence au package NuGet Aspire.Hosting.Azure.AppService dans votre projet hôte d’application.
  2. Dans le fichier AppHost.cs, appelez AddAzureAppServiceEnvironment() sur votre instance IDistributedApplicationBuilder pour créer un plan App Service. Notez que malgré le nom, cela ne provisionne pas de ressource App Service Environment.
  3. Sur la ressource de projet Functions, appelez .WithExternalHttpEndpoints(). Cela est nécessaire pour le déploiement avec l’intégration d’Aspire Azure App Service.
  4. Sur la ressource de projet Functions, appelez .PublishAsAzureAppServiceWebsite((infra, app) => app.Kind = "functionapp,linux") pour publier ce projet dans le plan.

Important

Vérifiez que vous définissez la app.Kind propriété sur "functionapp,linux". Ce paramètre garantit que la ressource est créée en tant qu’application de fonction, ce qui affecte les expériences d’utilisation de votre application.

L’exemple suivant montre un fichier minimal AppHost.cs pour un projet hôte d’application qui publie un projet Functions en tant qu’application de fonction :

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

Par défaut, cette configuration crée un plan Premium V3. Lorsque vous utilisez une référence SKU de plan App Service dédiée, la mise à l’échelle n’est pas basée sur les événements. Au lieu de cela, la mise à l’échelle est gérée via les paramètres du plan App Service. Si vous souhaitez modifier la référence SKU, vous pouvez le faire via la ConfigureInfrastructure méthode sur la ressource d’environnement. L’exemple suivant montre comment configurer un plan Elastic Premium :

using Azure.Provisioning.AppService;

var builder = DistributedApplication.CreateBuilder(args);
builder.AddAzureAppServiceEnvironment("functions-env");
    .ConfigureInfrastructure(infra =>
    {
        var plan = infra.GetProvisionableResources().OfType<AppServicePlan>().First();
        plan.Sku = new AppServiceSkuDescription
        {
           Name = "EP1",
           Tier = "ElasticPremium"
        };
    });
builder.AddAzureFunctionsProject<Projects.MyFunctionsProject>("MyFunctionsProject")
    .WithExternalHttpEndpoints()
    .PublishAsAzureAppServiceWebsite((infra, app) => app.Kind = "functionapp,linux");

Lorsque vous configurez le plan pour utiliser une référence SKU Elastic Premium, vous pouvez uniquement publier des projets Functions dans ce plan. Si votre projet hôte d’application inclut d’autres applications web, vous devez utiliser une référence SKU de plan App Service dédiée.

Considérations et bonnes pratiques

Tenez compte des points suivants lorsque vous évaluez l’intégration d’Azure Functions à Aspire :

  • La configuration du déclencheur et de la liaison via Aspire est actuellement limitée à des intégrations spécifiques. Pour plus d’informations, consultez La configuration de connexion avec Aspire dans cet article.

  • Le fichier de votre projet de Program.cs fonction doit utiliser la IHostApplicationBuilder version du démarrage de l’instance hôte. IHostApplicationBuilder vous permet d’appeler builder.AddServiceDefaults() pour ajouter des paramètres de service Aspire par défaut à votre projet Functions.

  • Aspire utilise OpenTelemetry pour la surveillance. Vous pouvez configurer Aspire pour exporter des données vers Azure Monitor via le projet par défaut du service.

    Dans de nombreux autres contextes Azure Functions, vous pouvez inclure l’intégration directe à Application Insights en inscrivant le service worker. Nous ne recommandons pas ce type d’intégration dans Aspire. Il peut entraîner des erreurs d’exécution avec la version 2.22.0 de Microsoft.ApplicationInsights.WorkerService, bien que la version 2.23.0 résout ce problème. Lorsque vous utilisez Aspire, supprimez toutes les intégrations Application Insights directes de votre projet Functions.

  • Pour les projets Functions inscrits dans une orchestration Aspire, la plupart de la configuration de l’application doit provenir du projet hôte d’application Aspire. Évitez de définir des éléments dans local.settings.json, autre que le FUNCTIONS_WORKER_RUNTIME paramètre. Si vous définissez la même variable d’environnement dans local.settings.json et Aspire, le système utilise la version d’Aspire.

  • Ne configurez pas l’émulateur stockage Azure pour les connexions dans local.settings.json. De nombreux modèles de démarrage Functions incluent l’émulateur par défaut pour AzureWebJobsStorage. Toutefois, la configuration de l’émulateur peut inviter certains outils de développement à démarrer une version de l’émulateur qui peut entrer en conflit avec la version utilisée par Aspire.