Compartilhar via


Azure Functions com Aspire

Aspire é uma pilha opinativa que simplifica o desenvolvimento de aplicativos distribuídos na nuvem. A integração do Aspire com o Azure Functions permite desenvolver, depurar e orquestrar um projeto do .NET do Azure Functions como parte do host do aplicativo Aspire.

Pré-requisitos

Configure seu ambiente de desenvolvimento para usar o Azure Functions com o Aspire:

  • Instale os pré-requisitos do Aspire.
    • O suporte completo para a integração do Azure Functions requer o Aspire 13.1 ou posterior. O Aspire 13.0 também inclui uma versão prévia de Aspire.Hosting.Azure.Functions, que funciona como versão Release Candidate com suporte para produção.
  • Instale as Azure Functions Core Tools.

Se você usar o Visual Studio, atualize para a versão 17.12 ou posterior. Você também deve ter a versão mais recente das ferramentas do Azure Functions para Visual Studio. Para verificar se há atualizações:

  1. Acesse Ferramentas>Opções.
  2. Em Projetos e Soluções, selecione Azure Functions.
  3. Selecione Verificar se há atualizações e instale atualizações conforme solicitado.

Estrutura da solução

Uma solução que usa o Azure Functions e o Aspire tem vários projetos, incluindo um projeto de host de aplicativo e um ou mais projetos do Functions.

O projeto de host do aplicativo é o ponto de entrada para seu aplicativo. Ele orquestra a instalação dos componentes do aplicativo, incluindo o projeto do Functions.

A solução normalmente também inclui um projeto de configurações padrão de serviços. Este projeto fornece um conjunto de serviços e configurações padrão a serem usados entre projetos em seu aplicativo.

Projeto de host do aplicativo

Para configurar com êxito a integração, verifique se o projeto de host do aplicativo atende aos seguintes requisitos:

  • O projeto de host do aplicativo deve referenciar Aspire.Hosting.Azure.Functions. Esse pacote define a lógica necessária para a integração.
  • O projeto de host do aplicativo precisa ter uma referência de projeto para cada projeto do Functions que você deseja incluir na orquestração.
  • No arquivo AppHost.cs do host do aplicativo, você deve incluir o projeto chamando AddAzureFunctionsProject<TProject>() na sua instância IDistributedApplicationBuilder. Você usa esse método em vez de usar o AddProject<TProject>() método usado para outros tipos de projeto no Aspire. Se você usar AddProject<TProject>(), o projeto do Functions não poderá ser iniciado corretamente.

O exemplo a seguir mostra um arquivo mínimo AppHost.cs para um projeto de host de aplicativo:

var builder = DistributedApplication.CreateBuilder(args);

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

builder.Build().Run();

Projeto do Azure Functions

Para configurar a integração com êxito, verifique se o projeto do Azure Functions atende aos seguintes requisitos:

O exemplo a seguir mostra um arquivo mínimo Program.cs para um projeto do Functions usado no Aspire:

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

var builder = FunctionsApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.ConfigureFunctionsWebApplication();

builder.Build().Run();

Este exemplo não inclui a configuração padrão do Application Insights que aparece em muitos outros Program.cs exemplos e nos modelos do Azure Functions. Em vez disso, você configura a integração do OpenTelemetry no Aspire chamando o builder.AddServiceDefaults método.

Para aproveitar ao máximo a integração, considere as seguintes diretrizes:

  • Não inclua nenhuma integração direta do Application Insights no projeto do Functions. O monitoramento no Aspire, por outro lado, é tratado por meio de seu suporte ao OpenTelemetry. Você pode configurar o Aspire para exportar dados para o Azure Monitor por meio do projeto de padrões de serviço.
  • Não defina configurações personalizadas de aplicativo no arquivo local.settings.json para o projeto Functions. A única configuração que deve estar em local.settings.json é "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated". Defina todas as outras configurações de aplicativo por meio do projeto de host do aplicativo.

Configuração de conexão com o Aspire

O projeto de host do aplicativo define recursos e ajuda você a criar conexões entre eles usando código. Esta seção mostra como configurar e personalizar conexões que seu projeto do Azure Functions usa.

O Aspire inclui permissões de conexão padrão que podem ajudá-lo a começar. No entanto, essas permissões podem não ser apropriadas ou suficientes para seu aplicativo.

Para cenários que usam o RBAC (controle de acesso baseado em função) do Azure, você pode personalizar permissões chamando o WithRoleAssignments() método no recurso de projeto. Quando você chama WithRoleAssignments(), todas as atribuições de função padrão são removidas e você deve definir explicitamente as atribuições de função de conjunto completo desejadas. Se você hospedar seu aplicativo nos Aplicativos de Contêiner do Azure, usar WithRoleAssignments() também exigirá que você chame AddAzureContainerAppEnvironment() em DistributedApplicationBuilder.

Armazenamento de host do Azure Functions

O Azure Functions requer uma conexão de armazenamento de host (AzureWebJobsStorage) para vários de seus principais comportamentos. Quando você chama AddAzureFunctionsProject<TProject>() no projeto de host do aplicativo, uma conexão AzureWebJobsStorage é criada por padrão e fornecida ao projeto de Funções. Essa conexão padrão usa o emulador de Armazenamento do Azure para execuções de desenvolvimento local e provisiona automaticamente uma conta de armazenamento quando ela é implantada. Para obter mais controle, você pode substituir essa conexão chamando .WithHostStorage() o recurso de projeto do Functions.

As permissões padrão que o Aspire define para a conexão de armazenamento do host dependem de se você chama WithHostStorage() ou não. A adição de WithHostStorage() remove uma atribuição de Colaborador da Conta de Armazenamento. A tabela a seguir lista as permissões padrão que o Aspire define para a conexão de armazenamento do host:

Conexão de armazenamento do host Funções padrão
Nenhuma chamada para WithHostStorage() Colaborador de Dados de Blob de Armazenamento,
Colaborador de Dados da Fila de Armazenamento,
Colaborador de Dados da Tabela de Armazenamento
Colaborador da Conta de Armazenamento
Chamar WithHostStorage() Colaborador de Dados de Blob de Armazenamento,
Colaborador de Dados da Fila de Armazenamento,
Colaborador de dados da tabela de armazenamento

O exemplo a seguir mostra um arquivo mínimo AppHost.cs para um projeto de host de aplicativo que substitui o armazenamento do host e especifica uma atribuição de função:

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

Observação

O Proprietário de Dados do Blob de Armazenamento é a função que recomendamos para as necessidades básicas da conexão de armazenamento do host. Seu aplicativo poderá ter problemas se a conexão com o serviço de blob tiver apenas o padrão do Aspire de Colaborador de dados de blob de armazenamento.

Para cenários de produção, inclua chamadas para ambos WithHostStorage() e WithRoleAssignments(). Em seguida, você pode definir essa função explicitamente, juntamente com todas as outras que precisar.

Como disparar e associar conexões

Seus gatilhos e associações fazem referência a conexões por nome. As seguintes integrações do Aspire fornecem essas conexões por meio de uma chamada para WithReference() no recurso do projeto.

Integração do Aspire Funções padrão
Armazenamento de Blobs do Azure Colaborador de Dados de Blob de Armazenamento,
Colaborador de Dados da Fila de Armazenamento,
Colaborador de dados da tabela de armazenamento
Armazenamento de Filas do Azure Colaborador de Dados de Blob de Armazenamento,
Colaborador de Dados da Fila de Armazenamento,
Colaborador de dados da tabela de armazenamento
Hubs de eventos do Azure Proprietário de dados dos Hubs de Eventos do Azure
Barramento de Serviço do Azure Proprietário de dados do Barramento de Serviço do Azure

O exemplo a seguir mostra um arquivo mínimo AppHost.cs para um projeto de hospedagem de app que configura um disparador de fila. Neste exemplo, o gatilho de fila correspondente tem sua Connection propriedade definida como MyQueueTriggerConnection, assim, a chamada para WithReference() especifica o nome.

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 outras integrações, chamadas para WithReference definem a configuração de uma maneira diferente. Eles disponibilizam a configuração para integrações de cliente do Aspire, mas não para gatilhos e associações. Para essas integrações, chame WithEnvironment() para passar as informações de conexão do gatilho ou da associação a ser resolvida.

O exemplo a seguir mostra como definir a variável MyBindingConnection de ambiente para um recurso que expõe uma expressão de cadeia de conexão:

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

Se você quiser que as integrações de cliente do Aspire e o sistema de gatilhos e associações usem uma conexão, você pode configurar ambos WithReference() e WithEnvironment().

Para alguns recursos, a estrutura de uma conexão pode ser diferente quando você a executa localmente e quando você a publica no Azure. No exemplo anterior, otherIntegration poderia ser um recurso executado como um emulador, portanto, ConnectionStringExpression retornaria uma cadeia de conexão do emulador. No entanto, quando o recurso é publicado, o Aspire pode configurar uma conexão baseada em identidade e ConnectionStringExpression retornar o URI do serviço. Nesse caso, para configurar conexões baseadas em identidade para o Azure Functions, talvez seja necessário fornecer um nome de variável de ambiente diferente.

O exemplo a seguir usa builder.ExecutionContext.IsPublishMode para adicionar condicionalmente o sufixo necessário:

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

Para obter detalhes sobre os formatos de conexão compatíveis com cada associação e as permissões que esses formatos exigem, consulte as páginas de referência da associação.

Hospedando o aplicativo

O Aspire dá suporte a duas maneiras diferentes de hospedar seu projeto do Functions no Azure:

Em ambos os casos, seu projeto é implantado como um contêiner. O Aspire encarrega-se de criar a imagem do contêiner para você e enviá-la para o Azure Container Registry.

Publicar como um aplicativo de contêiner

Por padrão, quando você publica um projeto Aspire no Azure, ele é implantado nos Aplicativos de Contêiner do Azure. O sistema configura regras de dimensionamento para seu projeto do Functions usando KEDA. Ao usar os Aplicativos de Contêiner do Azure, é necessária uma configuração adicional para chaves de função. Consulte as chaves de acesso nos Aplicativos de Contêiner do Azure para obter mais informações.

Chaves de acesso em Aplicativos de Contêiner do Azure

Vários cenários do Azure Functions usam chaves de acesso para fornecer uma mitigação básica contra acesso indesejado. Por exemplo, as funções de gatilho HTTP por padrão exigem que uma chave de acesso seja invocada, embora esse requisito possa ser desabilitado usando a AuthLevel propriedade. Consulte Trabalhar com chaves de acesso no Azure Functions para ver cenários que podem exigir uma chave.

Quando você implanta um projeto do Functions usando o Aspire para Aplicativos de Contêiner do Azure, o sistema não cria ou gerencia automaticamente as chaves de acesso do Functions. Se você precisar usar chaves de acesso, poderá gerenciá-las como parte da configuração do Host do Aplicativo. Esta seção mostra como criar um método de extensão que você pode chamar do arquivo do host do Program.cs aplicativo para criar e gerenciar chaves de acesso. Essa abordagem usa o Azure Key Vault para armazenar as chaves e montá-las no aplicativo de contêiner como segredos.

Observação

O comportamento aqui depende do ContainerApps provedor secreto, que só está disponível a partir da versão 4.1044.0 do host do Functions. Essa versão ainda não está disponível em todas as regiões e, até que seja, quando você publica seu projeto Aspire, a imagem base usada para o projeto do Functions pode não incluir as alterações necessárias.

Essas etapas exigem a versão 0.38.3 do Bicep ou posterior. Você pode verificar sua versão do Bicep executando bicep --version em um prompt de comando. Se você tiver a CLI do Azure instalada, poderá usar az bicep upgrade para atualizar rapidamente o Bicep para a versão mais recente.

Adicione os seguintes pacotes NuGet ao projeto de host do aplicativo:

Crie uma nova classe no projeto de host do aplicativo e inclua o seguinte código:

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

Em seguida, você pode usar este método no arquivo do host do aplicativo Program.cs.

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

Este exemplo usa um cofre de chaves padrão criado pelo método de extensão. Isso resulta em uma chave padrão e uma chave do sistema para uso com a extensão do Protocolo de contexto de modelo.

Para usar essas chaves de clientes, você precisa recuperá-las do cofre de chaves.

Publicar como um aplicativo de funções

Observação

A publicação como um aplicativo de funções requer a integração do Serviço de Aplicativo do Aspire azure, que está atualmente em versão prévia.

Você pode configurar o Aspire para implantar em um aplicativo de funções usando a integração do Serviço de Aplicativo do Azure do Aspire. Como o Aspire publica o projeto do Functions como um contêiner, o plano de hospedagem para seu aplicativo de funções deve dar suporte à implantação de aplicativos em contêineres.

Para publicar seu projeto do Aspire Functions como um aplicativo de funções, siga estas etapas:

  1. Adicione uma referência ao pacote NuGet Aspire.Hosting.Azure.AppService em seu projeto de host do aplicativo.
  2. No arquivo AppHost.cs, chame AddAzureAppServiceEnvironment() na sua instância IDistributedApplicationBuilder para criar um plano do Serviço de Aplicativo. Observe que, apesar do nome, ele não provisiona um recurso do Ambiente de Serviço de Aplicativo.
  3. No projeto Functions, chame o recurso .WithExternalHttpEndpoints(). Isso é necessário para implantar com a integração do Aspire Azure App Service.
  4. No recurso de projeto do Functions, chame .PublishAsAzureAppServiceWebsite((infra, app) => app.Kind = "functionapp,linux") para publicar esse projeto no plano.

Importante

Certifique-se de definir a app.Kind propriedade como "functionapp,linux". Essa configuração garante que o recurso seja criado como um aplicativo de funções, o que afeta as experiências para trabalhar com seu aplicativo.

O exemplo a seguir mostra um arquivo AppHost.cs mínimo para um projeto host de aplicativo que publica um projeto Functions como um aplicativo de funções:

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

Essa configuração cria um plano Premium V3. Ao utilizar um SKU dedicado de plano do Serviço de Aplicativos, o escalonamento não é baseado em eventos. Em vez disso, o dimensionamento é gerenciado por meio das configurações do plano do Serviço de Aplicativo.

Considerações e melhores práticas

Considere os seguintes pontos ao avaliar a integração do Azure Functions com o Aspire:

  • A configuração de gatilho e associação por meio do Aspire está atualmente limitada a integrações específicas. Para obter detalhes, consulte a configuração de conexão com o Aspire neste artigo.

  • O arquivo Program.cs do seu projeto de função deve usar a versão IHostApplicationBuilder de inicialização da instância do host. IHostApplicationBuilder permite que você chame builder.AddServiceDefaults() para adicionar padrões de serviço Aspire ao seu projeto Functions.

  • O Aspire usa o OpenTelemetry para monitoramento. Você pode configurar o Aspire para exportar dados para o Azure Monitor por meio do projeto de padrões de serviço.

    Em muitos outros contextos do Azure Functions, você pode incluir a integração direta com o Application Insights registrando o serviço de trabalho. Não recomendamos esse tipo de integração no Aspire. Isso pode levar a erros de runtime com a versão 2.22.0, Microsoft.ApplicationInsights.WorkerServiceembora a versão 2.23.0 resolva esse problema. Ao usar o Aspire, remova as integrações diretas do Application Insights do seu projeto do Functions.

  • Para projetos do Functions inscritos em uma orquestração Aspire, a maior parte da configuração da aplicação deve vir do projeto host de aplicativo do Aspire. Evite definir itens em local.settings.json, exceto pela configuração FUNCTIONS_WORKER_RUNTIME. Se você definir a mesma variável de ambiente local.settings.json no Aspire, o sistema usará a versão do Aspire.

  • Não configure o emulador de Armazenamento do Azure para conexões em local.settings.json. Muitos modelos de início do Functions incluem o emulador como padrão para AzureWebJobsStorage. No entanto, a configuração do emulador pode solicitar que algumas ferramentas de desenvolvedor iniciem uma versão do emulador que possa entrar em conflito com a versão usada pelo Aspire.