Partilhar via


Azure Functions com o Aspire

Aspire é uma stack estruturada que simplifica o desenvolvimento de aplicações distribuídas na nuvem. A integração do Aspire com o Azure Functions permite-lhe desenvolver, depurar e orquestrar um projeto .NET do Azure Functions como parte do anfitrião da aplicação Aspir.

Pré-requisitos

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

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. Vá paraOpções de ferramentas>.
  2. Em Projetos e Soluções, selecione Azure Functions.
  3. Selecione Verificar se há atualizações e instale as 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 configuração dos componentes do seu aplicativo, incluindo o projeto Functions.

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

Projeto de host de 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 fazer referência a Aspire.Hosting.Azure.Functions. Este pacote define a lógica necessária para a integração.
  • O projeto host do aplicativo precisa ter uma referência de projeto para cada projeto do Functions que você deseja incluir na orquestração.
  • No ficheiro de host do AppHost.cs da aplicação, 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 que você usa para outros tipos de projeto no Aspire. Se usar AddProject<TProject>(), o projeto Functions poderá não iniciar 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 com êxito a integração, 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 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 OpenTelemetry no Aspire chamando o builder.AddServiceDefaults método.

Para tirar o máximo proveito da integração, considere as seguintes diretrizes:

  • Não inclua integrações diretas do Application Insights no projeto Functions. Em vez disso, a monitorização no Aspire é tratada através do seu suporte 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 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 do aplicativo por meio do projeto de host do aplicativo.

Configuração da ligaçã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 ligação predefinidas que o podem ajudar 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 do projeto. Quando chamar WithRoleAssignments(), todas as atribuições de funções padrão são removidas, e você deve definir explicitamente todas as atribuições de funções que deseja. Se hospitares a tua aplicação em Apps de Contentores Azure, usar WithRoleAssignments() também requer que chames 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 comportamentos principais. Quando você chama AddAzureFunctionsProject<TProject>() seu projeto de host de aplicativo, uma AzureWebJobsStorage conexão é criada por padrão e fornecida ao projeto Functions. Essa conexão padrão usa o emulador de Armazenamento do Azure para execução 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 do projeto Functions.

As permissões padrão que o Aspire define para a conexão de armazenamento do host dependem se você chama WithHostStorage() ou não. Adicionar WithHostStorage() faz com que uma atribuição de Colaborador da Conta de Armazenamento seja removida. 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 predefinidas
Nenhuma chamada para WithHostStorage() Contribuidor de dados de Blob de armazenamento,
Contribuidor de dados da fila de armazenamento,
Contribuidor de dados da tabela de armazenamento,
Contribuidor da Conta de Armazenamento
Telefonar WithHostStorage() Contribuidor de dados de Blob de armazenamento,
Contribuidor de dados da fila de armazenamento,
Contribuidor 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

Administrador de Dados de Blob de Armazenamento é o papel que recomendamos para as necessidades essenciais da conexão de armazenamento do servidor. A sua aplicação pode encontrar problemas se a ligação ao serviço de blob tiver apenas o padrão Aspire de Colaborador de Dados do Blob de Armazenamento.

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

Conexões de gatilho e vinculação

Seus gatilhos e ligações fazem referência a conexões por nome. As seguintes integrações do Aspire proporcionam estas ligações através de uma chamada para o recurso do projeto WithReference().

Integração Aspire Funções predefinidas
Armazenamento de Blobs do Azure Contribuidor de dados de Blob de armazenamento,
Contribuidor de dados da fila de armazenamento,
Contribuidor de dados da tabela de armazenamento
Armazenamento de Filas do Azure Contribuidor de dados de Blob de armazenamento,
Contribuidor de dados da fila de armazenamento,
Contribuidor 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 host de aplicativo que configura um gatilho de fila. Neste exemplo, o gatilho de fila correspondente tem a propriedade Connection definida como MyQueueTriggerConnection, portanto, 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, as chamadas para WithReference definem a configuração de uma maneira diferente. Eles disponibilizam a configuração para integrações de clientes Aspire, mas não para gatilhos e ligações. Para essas integrações, chame WithEnvironment() para passar as informações de conexão para que o gatilho ou a ligação sejam resolvidos.

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 pretender que tanto as integrações do cliente Aspire como o sistema de gatilhos e ligações utilizem uma conexão, pode configurar ambos WithReference() e WithEnvironment().

Para alguns recursos, a estrutura de uma conexão pode ser diferente entre quando você a executa localmente e quando a publica no Azure. No exemplo anterior, otherIntegration poderia ser um recurso que é 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 ligação baseada em identidade e ConnectionStringExpression devolve 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 suportados por cada associação e as permissões que esses formatos exigem, consulte as páginas de referência da ligação.

Hospedando o aplicativo

O Aspire suporta duas formas diferentes de alojar o 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 de contentor para si e enviá-la para o Registo de Contentores do Azure.

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 o seu projeto Functions usando o KEDA. Ao usar os Aplicativos de Contêiner do Azure, é necessária uma configuração adicional para as teclas de função. Consulte Chaves de acesso em 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 atenuaçã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 cenários que podem exigir uma chave.

Quando você implanta um projeto do Functions usando o Aspire to Azure Container Apps, o sistema não cria ou gerencia automaticamente as chaves de acesso do Functions. Se precisar de utilizar chaves de acesso, pode geri-las como parte da configuração do Anfitrião da Aplicação. Esta seção mostra como criar um método de extensão que você pode chamar a partir do arquivo do host Program.cs do aplicativo para criar e gerenciar chaves de acesso. Essa abordagem usa o Cofre da Chave do Azure para armazenar as chaves e as monta no aplicativo de contêiner como segredos.

Observação

O comportamento aqui depende do provedor secreto ContainerApps , que só está disponível a partir da versão 4.1044.0de host do Functions. Esta versão ainda não está disponível em todas as regiões e, até que esteja, quando publicar o seu projeto Aspir, a imagem base utilizada para o projeto Functions poderá não incluir as alterações necessárias.

Estas etapas requerem a versão 0.38.3 ou posterior do Bicep. Você pode verificar sua versão do Bicep executando bicep --version a partir de 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 seu projeto de host de aplicativo:

Crie uma nova classe em seu projeto de host de 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, podes usar este método no ficheiro do host da aplicação Program.cs.

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

Este exemplo usa um cofre de chave padrão criado pelo método extension. Isso resulta em uma chave padrão e uma chave do sistema para uso com a extensão Model Context Protocol.

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

Publicar como um aplicativo de função

Observação

A publicação como uma aplicação de funções requer a integração do Serviço de Aplicações Aspire Azure, que está atualmente em versão prévia.

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

Para publicar o seu projeto Aspire Functions como uma aplicação funcional, siga estes passos:

  1. Adicione uma referência ao pacote NuGet Aspire.Hosting.Azure.AppService em seu projeto de host de aplicativo.
  2. No arquivo AppHost.cs, chame AddAzureAppServiceEnvironment() na sua IDistributedApplicationBuilder instância para criar um plano de Serviço de Aplicações. Note-se que, apesar do nome, este não provisiona um recurso de Ambiente de Serviço de Aplicações.
  3. No recurso do projeto Funções, chame .WithExternalHttpEndpoints(). Isto é exigido para a implantação com a integração do Aspire Azure App Service.
  4. No recurso do projeto Funções, 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ção, o que afeta as experiências de trabalho com seu aplicativo.

O exemplo a seguir mostra um arquivo mínimo AppHost.cs para um projeto de aplicação host que publica um projeto Functions como uma aplicação 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");

Esta configuração cria um plano Premium V3. Ao usar uma SKU de Plano de Serviço de Aplicações dedicado, o dimensionamento não depende de eventos. Em vez disso, o dimensionamento é gerenciado por meio das configurações do plano do Serviço de Aplicativo.

Considerações e práticas recomendadas

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

  • A configuração de acionamento e vinculação através do Aspire está atualmente limitada a integrações específicas. Para obter detalhes, consulte Configuração de conexão com o Aspire neste artigo.

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

  • O Aspire utiliza o OpenTelemetry para monitorização. 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, pode-se incluir a integração direta com o Application Insights ao registar o serviço de trabalho. Não recomendamos este tipo de integração no Aspire. Isso pode levar a erros de tempo de execução com a versão 2.22.0 do Microsoft.ApplicationInsights.WorkerService, embora a versão 2.23.0 resolva esse problema. Quando você estiver usando o Aspire, remova todas as integrações diretas do Application Insights do seu projeto do Functions.

  • Para os projetos do Functions inseridos numa orquestração do Aspire, a maior parte da configuração da aplicação deve vir do projeto hospedeiro da aplicação Aspire. Evite definir coisas em local.settings.json, exceto a configuração de FUNCTIONS_WORKER_RUNTIME. Se definir a mesma variável de ambiente em local.settings.json e no Aspire, o sistema utiliza a versão do Aspire.

  • Não configure o emulador de Armazenamento do Azure para qualquer conexão no local.settings.json. Muitos modelos iniciais do Functions incluem o emulador como padrão para AzureWebJobsStorage. No entanto, a configuração do emulador pode solicitar que algumas ferramentas do desenvolvedor iniciem uma versão do emulador que pode entrar em conflito com a versão que o Aspire usa.