Compartir por


Azure Functions con Aspire

Aspire es una pila con opiniones que simplifica el desarrollo de aplicaciones distribuidas en la nube. La integración de Aspire con Azure Functions le permite desarrollar, depurar y organizar un proyecto de .NET de Azure Functions como parte del host de la aplicación Aspire.

Prerrequisitos

Configure el entorno de desarrollo para usar Azure Functions con Aspire:

  • Instale los requisitos previos de Aspire.
    • La compatibilidad completa con la integración de Azure Functions requiere Aspire 13.1 o posterior. Aspire 13.0 también incluye una versión preliminar de Aspire.Hosting.Azure.Functions que actúa como una versión candidata para lanzamiento con soporte para producción.
  • Instale Azure Functions Core Tools.

Si usa Visual Studio, actualice a la versión 17.12 o posterior. También debe tener la versión más reciente de las herramientas de Azure Functions para Visual Studio. Para buscar actualizaciones:

  1. Vaya a Tools>Options (Herramientas > Opciones).
  2. En Proyectos y soluciones, seleccione Azure Functions.
  3. Seleccione Buscar actualizaciones e instale las actualizaciones según se le solicite.

Estructura de la solución

Una solución que usa Azure Functions y Aspire tiene varios proyectos, incluido un proyecto host de aplicación y uno o varios proyectos de Functions.

El proyecto de host de aplicaciones es el punto de entrada de la aplicación. Organiza la configuración de los componentes de la aplicación, incluido el proyecto de Functions.

Normalmente, la solución también incluye un proyecto configuración predeterminada del servicio. Este proyecto proporciona un conjunto de servicios y configuraciones predeterminados que se usarán en los proyectos de la aplicación.

Proyecto de host de aplicación

Para configurar correctamente la integración, asegúrese de que el proyecto host de la aplicación cumple los siguientes requisitos:

  • El proyecto host de la aplicación debe hacer referencia a Aspire.Hosting.Azure.Functions. Este paquete define la lógica necesaria para la integración.
  • El proyecto host de la aplicación debe tener una referencia de proyecto para cada proyecto de Functions que quiera incluir en la orquestación.
  • En el archivo AppHost.cs del host de aplicaciones, debe incluir el proyecto llamando a AddAzureFunctionsProject<TProject>() en la instancia IDistributedApplicationBuilder. Este método se usa en lugar de usar el AddProject<TProject>() método que se usa para otros tipos de proyecto en Aspire. Si usa AddProject<TProject>(), el proyecto de Functions no se puede iniciar correctamente.

En el ejemplo siguiente se muestra un archivo mínimo AppHost.cs para un proyecto host de aplicación:

var builder = DistributedApplication.CreateBuilder(args);

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

builder.Build().Run();

Proyecto de Azure Functions

Para configurar correctamente la integración, asegúrese de que el proyecto de Azure Functions cumple los siguientes requisitos:

En el ejemplo siguiente se muestra un archivo mínimo Program.cs para un proyecto de Functions usado en Aspire:

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

var builder = FunctionsApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.ConfigureFunctionsWebApplication();

builder.Build().Run();

En este ejemplo no se incluye la configuración predeterminada de Application Insights que aparece en muchos otros Program.cs ejemplos y en las plantillas de Azure Functions. En su lugar, configurará la integración de OpenTelemetry en Aspire llamando al método builder.AddServiceDefaults.

Para sacar el máximo partido de la integración, tenga en cuenta las siguientes directrices:

  • No incluya ninguna integración directa de Application Insights en el proyecto de Functions. La supervisión en Aspire, en lugar de eso, se controla a través de su compatibilidad con OpenTelemetry. Puede configurar Aspire para exportar datos a Azure Monitor a través del proyecto predeterminado del servicio.
  • No defina configuraciones personalizadas de la aplicación en el archivo local.settings.json para el proyecto de Functions. La única configuración que debe estar en local.settings.json es "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated". Establezca todas las demás configuraciones de aplicación a través del proyecto host de la aplicación.

Configuración de conexión con Aspire

El proyecto host de la aplicación define recursos y le ayuda a crear conexiones entre ellos mediante código. En esta sección se muestra cómo configurar y personalizar las conexiones que usa el proyecto de Azure Functions.

Aspire incluye permisos de conexión predeterminados que pueden ayudarle a empezar. Sin embargo, es posible que estos permisos no sean adecuados o suficientes para la aplicación.

En escenarios que usan el control de acceso basado en rol (RBAC) de Azure, puede personalizar los permisos llamando al WithRoleAssignments() método en el recurso del proyecto. Al llamar a WithRoleAssignments(), se eliminan todas las asignaciones de roles predeterminadas y debe definir explícitamente el conjunto completo de asignaciones de roles que desee. Si hospeda la aplicación en Azure Container Apps, el uso de WithRoleAssignments() también requiere que llame a AddAzureContainerAppEnvironment() en DistributedApplicationBuilder.

Almacenamiento de host de Azure Functions

Azure Functions requiere una conexión de almacenamiento de host (AzureWebJobsStorage) para varios de sus comportamientos principales. Cuando llamas a AddAzureFunctionsProject<TProject>() en el proyecto host de tu aplicación, se crea una conexión AzureWebJobsStorage por defecto y se proporciona al proyecto de Funciones. Esta conexión predeterminada usa el emulador de Azure Storage para las ejecuciones de desarrollo local y aprovisiona automáticamente una cuenta de almacenamiento cuando se implementa. Para obtener más control, puede reemplazar esta conexión llamando .WithHostStorage() al recurso del proyecto de Functions.

Los permisos predeterminados que Aspire establece para la conexión de almacenamiento del host dependen de si llamas a WithHostStorage() o no. Agregar WithHostStorage() quita una asignación de colaborador de la cuenta de almacenamiento. En la tabla siguiente se enumeran los permisos predeterminados que Aspire establece para la conexión de almacenamiento del host:

Conexión de almacenamiento del servidor Roles predeterminados
Sin llamada a WithHostStorage() Colaborador de datos de Storage Blob,
Colaborador de datos de la cola de Storage,
Colaborador de datos de tabla de Storage,
Colaborador de la cuenta de almacenamiento
Llamadas a WithHostStorage() Colaborador de datos de Storage Blob,
Colaborador de datos de la cola de Storage,
Colaborador de datos de tabla de Storage

En el ejemplo siguiente se muestra un archivo mínimo AppHost.cs para un proyecto host de aplicación que reemplaza el almacenamiento de host y especifica una asignación de roles:

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

Nota:

El propietario de datos de blobs de almacenamiento es el rol que se recomienda para las necesidades básicas de la conexión de almacenamiento de host. Es posible que la aplicación encuentre problemas si la conexión con Blob service solo tiene el valor predeterminado de Aspire de colaborador de datos de blobs de almacenamiento.

En escenarios de producción, incluya llamadas a WithHostStorage() y WithRoleAssignments(). A continuación, puede establecer este rol explícitamente, junto con cualquier otro que necesite.

Conexiones de desencadenador y enlace

Los desencadenadores y enlaces hacen referencia a las conexiones por nombre. Las siguientes integraciones de Aspire proporcionan estas conexiones a través de una llamada a WithReference() en el recurso del proyecto:

Integración de Aspire Roles predeterminados
Azure Blob Storage Colaborador de datos de Storage Blob,
Colaborador de datos de la cola de Storage,
Colaborador de datos de tabla de Storage
Azure Queue Storage Colaborador de datos de Storage Blob,
Colaborador de datos de la cola de Storage,
Colaborador de datos de tabla de Storage
Azure Event Hubs Propietario de los datos de Azure Event Hubs
Azure Service Bus Propietario de los datos de Azure Service Bus

En el ejemplo siguiente se muestra un archivo mínimo AppHost.cs para un proyecto host de aplicación que configura un desencadenador de cola. En este ejemplo, el desencadenador de cola correspondiente tiene su propiedad Connection establecida en MyQueueTriggerConnection, por lo que la llamada a WithReference() especifica el nombre.

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

Para otras integraciones, las llamadas a WithReference establecen la configuración de otra manera. Hacen que la configuración esté disponible para las integraciones de cliente Aspire, pero no para desencadenadores y vinculaciones. Para estas integraciones, llame a WithEnvironment() para pasar la información de conexión para que el desencadenador o el enlace se resuelvan.

En el ejemplo siguiente se muestra cómo establecer la variable MyBindingConnection de entorno para un recurso que expone una expresión de cadena de conexión:

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

Si desea que tanto las integraciones de cliente de Aspire como el sistema de desencadenadores y enlaces usen una conexión, puede configurar ambos WithReference() y WithEnvironment().

Para algunos recursos, la estructura de una conexión puede ser diferente entre la ejecución local y la publicación en Azure. En el ejemplo anterior, otherIntegration podría ser un recurso que se ejecuta como un emulador, por lo que ConnectionStringExpression devolvería una cadena de conexión del emulador. Sin embargo, cuando se publica el recurso, Aspire podría configurar una conexión basada en identidades y ConnectionStringExpression devolvería el URI del servicio. En este caso, para configurar conexiones basadas en identidades para Azure Functions, es posible que tenga que proporcionar un nombre de variable de entorno diferente.

En el ejemplo siguiente se usa builder.ExecutionContext.IsPublishMode para agregar condicionalmente el sufijo necesario:

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

Para obtener más información sobre los formatos de conexión que admite cada enlace y los permisos que requieren esos formatos, consulte las páginas de referencia del enlace.

Hospedaje de la aplicación

Aspire admite dos maneras diferentes de hospedar el proyecto de Functions en Azure:

En ambos casos, el proyecto se implementa como un contenedor. Aspire se encarga de compilar la imagen de contenedor para usted e insertarla en Azure Container Registry.

Publicar como app contenedor

De forma predeterminada, al publicar un proyecto Aspire en Azure, se implementa en Azure Container Apps. El sistema configura reglas de escalado para el proyecto de Functions mediante KEDA. Al usar Azure Container Apps, se necesita una configuración adicional para las claves de función. Consulte Claves de acceso en Azure Container Apps para más información.

Claves de acceso en Azure Container Apps

Varios escenarios de Azure Functions usan claves de acceso para proporcionar una mitigación básica contra el acceso no deseado. Por ejemplo, las funciones de desencadenador HTTP de forma predeterminada requieren que se invoque una clave de acceso, aunque este requisito se puede deshabilitar mediante la AuthLevel propiedad . Consulte Trabajar con claves de acceso en Azure Functions para ver escenarios que pueden requerir una clave.

Al implementar un proyecto de Functions mediante Aspire a Azure Container Apps, el sistema no crea ni administra automáticamente las claves de acceso de Functions. Si necesita usar claves de acceso, puede administrarlas como parte de la configuración del host de la aplicación. En esta sección se muestra cómo crear un método de extensión al que puede llamar desde el archivo del host de Program.cs la aplicación para crear y administrar claves de acceso. Este enfoque usa Azure Key Vault para almacenar las claves y montarlas en la aplicación contenedora como secretos.

Nota:

El comportamiento aquí se basa en el ContainerApps proveedor de secretos, que solo está disponible a partir de la versión 4.1044.0 del host de Functions. Esta versión aún no está disponible en todas las regiones y, hasta que sea así, al publicar el proyecto Aspire, es posible que la imagen base usada para el proyecto de Functions no incluya los cambios necesarios.

Estos pasos requieren la versión 0.38.3 de Bicep o posterior. Para comprobar la versión de Bicep, ejecute bicep --version desde una ventana de comandos. Si tiene instalada la CLI de Azure, puede usar az bicep upgrade para actualizar rápidamente Bicep a la versión más reciente.

Agregue los siguientes paquetes NuGet al proyecto host de la aplicación:

Cree una nueva clase en el proyecto host de la aplicación e incluya el código siguiente:

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

A continuación, puede usar este método en el archivo Program.cs del host de la aplicación.

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

En este ejemplo se usa un almacén de claves predeterminado creado por el método de extensión. Da como resultado una clave predeterminada y una clave del sistema para su uso con la extensión Model Context Protocol.

Para usar estas claves que provienen de los clientes, debe obtenerlas del almacén de claves.

Publicación como una aplicación de funciones

Nota:

La publicación como aplicación de funciones requiere la integración de Aspire Azure App Service, que se encuentra actualmente en versión preliminar.

Puede configurar Aspire para implementar en una aplicación de funciones mediante la integración de Aspire Azure App Service. Dado que Aspire publica el proyecto de Functions como contenedor, el plan de hospedaje de la aplicación de funciones debe admitir la implementación de aplicaciones en contenedor.

Para publicar el proyecto de Aspire Functions como una aplicación de funciones, siga estos pasos:

  1. Agregue una referencia al paquete NuGet Aspire.Hosting.Azure.AppService en el proyecto host de la aplicación.
  2. En el archivo AppHost.cs, llame a AddAzureAppServiceEnvironment() en la instancia IDistributedApplicationBuilder para crear un plan de App Service. Tenga en cuenta que, a pesar del nombre, esto no aprovisiona un recurso de App Service Environment.
  3. En el recurso del proyecto Functions, llame a .WithExternalHttpEndpoints(). Esto es necesario para la implementación con la integración de Aspire Azure App Service.
  4. En el recurso del proyecto de Functions, llame a .PublishAsAzureAppServiceWebsite((infra, app) => app.Kind = "functionapp,linux") para publicar ese proyecto en el plan.

Importante

Asegúrese de establecer la propiedad app.Kind a "functionapp,linux". Esta configuración garantiza que el recurso se crea como una aplicación de funciones, lo que afecta a las experiencias para trabajar con la aplicación.

En el ejemplo siguiente se muestra un archivo mínimo AppHost.cs para un proyecto host de aplicación que publica un proyecto de Functions como una aplicación de funciones:

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

Esta configuración crea un plan Premium V3. Cuando se usa una SKU de plan de App Service dedicado, el escalado no se basa en eventos. En su lugar, el escalado se administra mediante la configuración del plan de App Service.

Consideraciones y procedimientos recomendados

Tenga en cuenta los siguientes puntos al evaluar la integración de Azure Functions con Aspire:

  • La configuración de activación y vinculación mediante Aspire está actualmente limitada a integraciones específicas. Para obtener más información, consulte Configuración de conexión con Aspire en este artículo.

  • El archivo Program.cs debe utilizar la versión IHostApplicationBuilder de inicio de instancia de host. IHostApplicationBuilder permite llamar builder.AddServiceDefaults() para agregar valores predeterminados del servicio Aspire al proyecto de Functions.

  • Aspire usa OpenTelemetry para la supervisión. Puede configurar Aspire para exportar datos a Azure Monitor a través del proyecto predeterminado del servicio.

    En muchos otros contextos de Azure Functions, puede incluir la integración directa con Application Insights mediante el registro del servicio de trabajo. No se recomienda este tipo de integración en Aspire. Puede provocar errores en tiempo de ejecución con la versión 2.22.0 de Microsoft.ApplicationInsights.WorkerService, aunque la versión 2.23.0 soluciona este problema. Cuando use Aspire, quite las integraciones directas de Application Insights del proyecto de Functions.

  • En el caso de los proyectos de funciones incluidos en una orquestación de Aspire, la mayor parte de la configuración de la aplicación debe proceder del proyecto host de aplicaciones de Aspire. Evite establecer configuraciones en local.settings.json, aparte de la configuración de FUNCTIONS_WORKER_RUNTIME. Si establece la misma variable de entorno en local.settings.json y Aspire, el sistema usa la versión Aspire.

  • No configure el emulador de Azure Storage para ninguna conexión de local.settings.json. Muchas plantillas de inicio de Functions incluyen el emulador como valor predeterminado para AzureWebJobsStorage. Sin embargo, la configuración del emulador puede pedir a algunos desarrolladores herramientas para iniciar una versión del emulador que pueda entrar en conflicto con la versión que usa Aspire.