Condividi tramite


Funzioni di Azure con Aspire (anteprima)

Aspire è uno stack tecnologico con scelte predefinite che semplifica lo sviluppo di applicazioni distribuite nel cloud. L'integrazione di Aspire con Azure Functions consente di sviluppare, eseguire il debug e orchestrare un progetto .NET di Azure Functions come parte dell'host dell'app Aspire.

Importante

L'integrazione di Aspira con Funzioni di Azure è attualmente in anteprima ed è soggetta a modifiche.

Prerequisiti

Configurare l'ambiente di sviluppo per l'uso di Azure Functions con Aspire:

  • Installare .NET 9 SDK e Aspire 9.0 o versione successiva. Anche se è necessario .NET 9 SDK, Aspira 9.0 supporta i framework .NET 8 e .NET 9.
  • Se si usa Visual Studio, eseguire l'aggiornamento alla versione 17.12 o successiva. È anche necessario avere la versione più recente degli strumenti di Funzioni di Azure per Visual Studio. Per verificare la disponibilità di aggiornamenti:
    1. Passare a Strumenti>Opzioni.
    2. In Progetti e soluzioni selezionare Funzioni di Azure.
    3. Selezionare Controlla aggiornamenti e installare gli aggiornamenti come richiesto.

Annotazioni

L'integrazione di Funzioni di Azure con Aspire non supporta ancora .NET 10 Preview.

Struttura della soluzione

Una soluzione che usa Funzioni di Azure e Aspira ha più progetti, tra cui un progetto host dell'app e uno o più progetti di Funzioni.

Il progetto host dell'app è il punto di ingresso per l'applicazione. Orchestra la configurazione dei componenti dell'applicazione, incluso il progetto Funzioni.

La soluzione include in genere anche un progetto predefinito del servizio . Questo progetto fornisce un set di servizi e configurazioni predefiniti da usare tra progetti nell'applicazione.

Progetto di hosting dell'app

Per configurare correttamente l'integrazione, assicurarsi che il progetto host dell'app soddisfi i requisiti seguenti:

  • Il progetto host dell'app deve fare riferimento a Aspire.Hosting.Azure.Functions. Questo pacchetto definisce la logica necessaria per l'integrazione.
  • Il progetto host dell'app deve avere un riferimento di progetto per ogni progetto Functions che si vuole includere nell'orchestra.
  • Nel file dell'host dell'app AppHost.cs, è necessario includere il progetto chiamando AddAzureFunctionsProject<TProject>() sull'istanza IDistributedApplicationBuilder. Questo metodo viene usato invece di usare il AddProject<TProject>() metodo usato per altri tipi di progetto in Aspira. Se si usa AddProject<TProject>(), il progetto funzioni non può essere avviato correttamente.

L'esempio seguente mostra un file minimo AppHost.cs per un progetto host dell'app:

var builder = DistributedApplication.CreateBuilder(args);

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

builder.Build().Run();

Progetto funzioni di Azure

Per configurare correttamente l'integrazione, assicurarsi che il progetto funzioni di Azure soddisfi i requisiti seguenti:

L'esempio seguente mostra un file minimo Program.cs per un progetto di Funzioni usato in Aspira:

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

var builder = FunctionsApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.ConfigureFunctionsWebApplication();

builder.Build().Run();

Questo esempio non include la configurazione predefinita di Application Insights visualizzata in molti altri Program.cs esempi e nei modelli di Funzioni di Azure. Invece, l'integrazione di OpenTelemetry in Aspire si configura chiamando il metodo builder.AddServiceDefaults.

Per sfruttare al meglio l'integrazione, prendere in considerazione le linee guida seguenti:

  • Non includere integrazioni dirette di Application Insights nel progetto Funzioni. Il monitoraggio in Aspira è invece gestito tramite il supporto OpenTelemetry. È possibile configurare Aspire per esportare i dati in Azure Monitor tramite le impostazioni predefinite del servizio.
  • Non definire le impostazioni dell'app personalizzate nel file local.settings.json per il progetto delle funzioni. L'unica impostazione che deve trovarsi in local.settings.json è "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated". Impostare tutte le altre configurazioni dell'app tramite il progetto host dell'app.

Configurazione della connessione con Aspira

Il progetto host dell'app definisce le risorse e consente di creare connessioni tra di esse usando il codice. Questa sezione illustra come configurare e personalizzare le connessioni usate dal progetto funzioni di Azure.

Aspire include le autorizzazioni di connessione predefinite che consente di iniziare. Tuttavia, queste autorizzazioni potrebbero non essere appropriate o sufficienti per l'applicazione.

Per gli scenari che usano il controllo degli accessi in base al ruolo di Azure, è possibile personalizzare le autorizzazioni chiamando il WithRoleAssignments() metodo nella risorsa del progetto. Quando si chiama WithRoleAssignments(), tutte le assegnazioni di ruolo predefinite vengono rimosse ed è necessario definire in modo esplicito le assegnazioni di ruolo del set completo desiderate. Se si ospita l'applicazione su App contenitore di Azure, usare WithRoleAssignments() richiede anche di chiamare AddAzureContainerAppEnvironment() su DistributedApplicationBuilder.

Archiviazione delle Funzioni di Azure host

Funzioni di Azure richiede una connessione di archiviazione host (AzureWebJobsStorage) per diversi comportamenti principali. Quando si chiama AddAzureFunctionsProject<TProject>() nel progetto host dell'app, viene creata una connessione AzureWebJobsStorage per impostazione predefinita e fornita al progetto di funzioni. Questa connessione predefinita usa l'emulatore di Archiviazione di Azure per le esecuzioni di sviluppo locale e effettua automaticamente il provisioning di un account di archiviazione quando viene distribuito. Per un maggiore controllo, è possibile sostituire questa connessione chiamando .WithHostStorage() sulla risorsa del progetto funzioni.

Le autorizzazioni predefinite che Aspire imposta per la connessione di archiviazione dell'host dipendono dal fatto se si chiama WithHostStorage() o meno. L'aggiunta di WithHostStorage() comporta la rimozione di un'assegnazione di Collaboratore account di archiviazione. Nella tabella seguente sono elencate le autorizzazioni predefinite impostate da Aspira per la connessione all'archiviazione host:

Connessione all'archiviazione del server Ruoli predefiniti
Nessuna chiamata a WithHostStorage() Collaboratore ai dati del BLOB di archiviazione,
Collaboratore ai dati della coda di archiviazione,
Collaboratore ai dati della tabella di archiviazione
Collaboratore account di archiviazione
Chiamata WithHostStorage() Collaboratore ai dati del BLOB di archiviazione,
Collaboratore ai dati della coda di archiviazione,
Collaboratore ai dati della tabella di archiviazione

L'esempio seguente mostra un file minimo AppHost.cs per un progetto host dell'app che sostituisce l'archiviazione host e specifica un'assegnazione di ruolo:

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

Annotazioni

Storage Blob Data Owner è il ruolo consigliato per le esigenze di base della connessione con l'archiviazione host. L'app potrebbe riscontrare problemi se la connessione al servizio BLOB ha solo l'impostazione predefinita di Aspire per collaboratore ai dati dei BLOB di archiviazione.

Per gli scenari di produzione, includere chiamate sia a WithHostStorage() che a WithRoleAssignments(). È quindi possibile impostare questo ruolo in modo esplicito, insieme a qualsiasi altro elemento necessario.

Connessioni di trigger e binding

I trigger e i binding fanno riferimento alle connessioni in base al nome. Le seguenti integrazioni Aspire forniscono queste connessioni tramite una chiamata a WithReference() sulla risorsa del progetto.

Integrazione Aspire Ruoli predefiniti
Archiviazione BLOB di Azure Collaboratore ai dati del BLOB di archiviazione,
Collaboratore ai dati della coda di archiviazione,
Collaboratore ai dati della tabella di archiviazione
Archiviazione code di Azure Collaboratore ai dati del BLOB di archiviazione,
Collaboratore ai dati della coda di archiviazione,
Collaboratore ai dati della tabella di archiviazione
Hub eventi di Azure Proprietario dei dati di Hub eventi di Azure
Bus di servizio di Azure Proprietario dei dati del Service Bus di Azure

L'esempio seguente mostra un file AppHost.cs minimale per un progetto di hosting dell'app che configura un trigger della coda. In questo esempio, il trigger della coda corrispondente ha la proprietà Connection impostata su MyQueueTriggerConnection, quindi la chiamata a WithReference() specifica il 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();

Per altre integrazioni, effettua chiamate a WithReference per impostare la configurazione in modo diverso. Rendono disponibile la configurazione per le integrazioni di Aspire client, ma non per i trigger e le associazioni. Per queste integrazioni, chiamare WithEnvironment() per passare le informazioni di connessione per il trigger o il collegamento da risolvere.

Nell'esempio seguente viene illustrato come impostare la variabile MyBindingConnection di ambiente per una risorsa che espone un'espressione di stringa di connessione:

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

Se desideri che le integrazioni del client Aspire e il sistema di trigger e associazioni usino una connessione, è possibile configurare sia WithReference() che WithEnvironment().

Per alcune risorse, la struttura di una connessione potrebbe essere diversa tra l'esecuzione locale e la pubblicazione in Azure. Nell'esempio precedente, otherIntegration potrebbe essere una risorsa eseguita come emulatore, quindi ConnectionStringExpression restituirebbe un emulatore stringa di connessione. Tuttavia, quando la risorsa viene pubblicata, Aspire potrebbe configurare una connessione basata sull'identità e ConnectionStringExpression restituirà l'URI del servizio. In questo caso, per configurare le connessioni basate sull'identità per Funzioni di Azure, potrebbe essere necessario specificare un nome di variabile di ambiente diverso.

Nell'esempio seguente viene usato per aggiungere in modo condizionale il suffisso builder.ExecutionContext.IsPublishMode necessario:

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

Per informazioni dettagliate sui formati di connessione supportati da ogni associazione e sulle autorizzazioni necessarie per tali formati, vedere le pagine di riferimento dell'associazione.

Hosting dell'applicazione

Per impostazione predefinita, quando si pubblica un progetto di Funzioni di Azure in Azure, viene distribuito in App Azure Container.

Durante il periodo di anteprima, le risorse dell'app contenitore non supportano il ridimensionamento basato su eventi. Funzioni di Azure supporto non è disponibile per le app distribuite in questa modalità. Se hai bisogno di aprire un ticket di supporto, seleziona il tipo di risorsa Azure Container Apps.

Chiavi di accesso

Diversi scenari di Funzioni di Azure usano le chiavi di accesso per fornire una mitigazione di base contro l'accesso indesiderato. Ad esempio, per impostazione predefinita, le funzioni trigger HTTP richiedono che venga richiamata una chiave di accesso, anche se questo requisito può essere disabilitato tramite la AuthLevel proprietà . Vedere Usare le chiavi di accesso in Funzioni di Azure per scenari che potrebbero richiedere una chiave.

Quando si distribuisce un progetto di Funzioni usando Aspira alle app contenitore di Azure, il sistema non crea o gestisce automaticamente le chiavi di accesso di Funzioni. Se è necessario usare le chiavi di accesso, è possibile gestirle come parte della configurazione dell'host app. Questa sezione illustra come creare un metodo di estensione che è possibile chiamare dal file dell'host dell'app per creare e gestire le chiavi di Program.cs accesso. Questo approccio usa Azure Key Vault per archiviare le chiavi e montarle nell'app contenitore come segreti.

Annotazioni

Il comportamento qui si basa sul fornitore di segreti ContainerApps, disponibile solo a partire dalla versione host di Funzioni 4.1044.0. Questa versione non è ancora disponibile in tutte le aree e fino a quando non si pubblica il progetto Aspira, l'immagine di base usata per il progetto funzioni potrebbe non includere le modifiche necessarie.

Questi passaggi richiedono la versione Bicep 0.38.3 o successiva. È possibile controllare la versione di Bicep eseguendo bicep --version da un prompt dei comandi. Se è installata l'interfaccia della riga di comando di Azure, è possibile usare az bicep upgrade per aggiornare rapidamente Bicep alla versione più recente.

Aggiungere i pacchetti NuGet seguenti al progetto host dell'app:

Creare una nuova classe nel progetto host dell'app e includere il codice seguente:

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

È quindi possibile usare questo metodo nel file dell'host dell'app Program.cs :

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

Questo esempio usa un Key Vault predefinito creato dal metodo di estensione. Viene restituita una chiave predefinita e una chiave di sistema da usare con l'estensione Model Context Protocol.

Per usare queste chiavi dei client, è necessario recuperarle dal vault delle chiavi.

Considerazioni e procedure consigliate

Quando si valuta l'integrazione di Funzioni di Azure con Aspira, prendere in considerazione i punti seguenti:

  • Il supporto per l'integrazione è attualmente in anteprima.

  • La configurazione di trigger e binding tramite Aspira è attualmente limitata a integrazioni specifiche. Per informazioni dettagliate, vedere Configurazione della connessione con Aspire in questo articolo.

  • Il file del progetto di funzione Program.cs deve usare la versione IHostApplicationBuilder dell'avvio dell'istanza host. IHostApplicationBuilder consente di chiamare builder.AddServiceDefaults() per aggiungere i valori predefiniti del servizio Aspire al progetto di Funzioni.

  • Aspire utilizza OpenTelemetry per il monitoraggio. È possibile configurare Aspire per esportare i dati in Azure Monitor tramite il progetto predefinito di servizio.

    In molti altri contesti di Funzioni di Azure, è possibile includere l'integrazione diretta con Application Insights registrando il servizio di lavoro. Non consigliamo questo tipo di integrazione in Aspira. Può causare errori di runtime con la versione 2.22.0 di Microsoft.ApplicationInsights.WorkerService, anche se la versione 2.23.0 risolve questo problema. Quando si usa Aspire, rimuovere tutte le integrazioni dirette di Application Insights dal progetto Functions.

  • Per i progetti di Funzioni inseriti in un'orchestrazione Aspira, la maggior parte della configurazione dell'applicazione dovrebbe provenire dal progetto host dell'app Aspira. Evitare di impostare elementi in local.settings.json, ad eccezione dell'impostazione FUNCTIONS_WORKER_RUNTIME . Se si imposta la stessa variabile di ambiente in local.settings.json e Aspira, il sistema usa la versione Aspira.

  • Non configurare l'emulatore di Archiviazione di Azure per le connessioni in local.settings.json. Molti modelli di avvio di Funzioni includono l'emulatore come predefinito per AzureWebJobsStorage. Tuttavia, la configurazione dell'emulatore può richiedere ad alcuni strumenti di sviluppo di avviare una versione dell'emulatore in conflitto con la versione usata da Aspire.