Przewodnik po kodzie: aplikacja bezserwerowa z usługą Functions

Azure Event Hubs
Azure Functions

Modele bezserwerowe abstrakcji kodu z podstawowej infrastruktury obliczeniowej, dzięki czemu deweloperzy mogą skupić się na logice biznesowej bez rozbudowanej konfiguracji. Kod bezserwerowy zmniejsza koszty, ponieważ płacisz tylko za zasoby i czas trwania wykonywania kodu.

Model oparty na zdarzeniach bezserwerowych pasuje do sytuacji, w których określone zdarzenie wyzwala zdefiniowaną akcję. Na przykład odbieranie przychodzącego komunikatu urządzenia wyzwala magazyn do późniejszego użycia lub aktualizacja bazy danych wyzwala dalsze przetwarzanie.

Aby ułatwić eksplorowanie technologii bezserwerowych platformy Azure na platformie Azure, firma Microsoft opracowała i przetestowała aplikację bezserwerową korzystającą z usługi Azure Functions. W tym artykule opisano kod rozwiązania funkcji bezserwerowych i opisano decyzje projektowe, szczegóły implementacji i niektóre z "gotchas", które mogą wystąpić.

Eksplorowanie rozwiązania

Dwuczęściowe rozwiązanie opisuje hipotetyczny system dostarczania dronów. Drony podczas lotu wysyłają stan do chmury, gdzie te komunikaty są przechowywane do użycia w przyszłości. Aplikacja internetowa umożliwia użytkownikom pobieranie komunikatów w celu uzyskania najnowszego stanu urządzeń.

Kod tego rozwiązania można pobrać z usługi GitHub.

W tym przewodniku założono podstawową znajomość następujących technologii:

Nie musisz być ekspertem w zakresie usług Functions i Event Hubs, ale musisz rozumieć ich funkcje na wysokim poziomie. Poniżej przedstawiono kilka wartościowych zasobów ułatwiających rozpoczęcie pracy:

Omówienie scenariusza

Diagram of the functional blocks

Firma Fabrikam zarządza flotą dronów dla usługi dostarczania za pomocą dronów. Aplikacja składa się z dwóch głównych obszarów funkcjonalnych:

  • Pozyskiwanie zdarzeń. Podczas lotu drony wysyłają komunikaty o stanie do punktu końcowego w chmurze. Aplikacja pozyskuje i przetwarza te komunikaty oraz zapisuje wyniki w bazie danych zaplecza (Azure Cosmos DB). Urządzenia wysyłają komunikaty w formacie protocol buffer (protobuf). Protobuf to wydajny, samoopisujący format serializacji.

    Te komunikaty zawierają aktualizacje częściowe. W stałych odstępach czasu każdy dron wysyła komunikat „ramka kluczowa” zawierający wszystkie pola stanu. Między ramkami kluczowymi komunikaty o stanie zawierają tylko te pola, które uległy zmianie od czasu ostatniego komunikatu. To zachowanie jest typowe dla wielu urządzeń IoT, które muszą oszczędzać przepustowość i energię.

  • Aplikacja internetowa. Aplikacja internetowa umożliwia użytkownikom wyszukiwanie urządzenia i wykonywanie zapytań o ostatni znany stan urządzenia. Użytkownicy muszą zalogować się do aplikacji i uwierzytelnić się przy użyciu identyfikatora Entra firmy Microsoft. Aplikacja zezwala na żądania tylko od użytkowników, którzy mają uprawnienia dostępu do aplikacji.

Poniżej przedstawiono zrzut ekranu aplikacji internetowej z wyświetlonym wynikiem zapytania:

Screenshot of client app

Projektowanie aplikacji

Firma Fabrikam postanowiła zaimplementować logikę biznesową aplikacji przy użyciu usługi Azure Functions. Usługa Azure Functions jest przykładem rozwiązania typu „funkcje jako usługa" (Functions as a Service, FaaS). W tym modelu obliczeniowym funkcja jest fragmentem kodu, który jest wdrażany w chmurze i działa w środowisku hostingu. To środowisko hostingu całkowicie eliminuje serwery używane do uruchamiania kodu.

Dlaczego warto wybrać podejście bezserwerowe?

Architektura bezserwerowa z usługą Functions jest przykładem architektury sterowanej zdarzeniami. Kod funkcji jest wyzwalany przez zdarzenie zewnętrzne dla funkcji — w tym przypadku komunikat z drona lub żądanie HTTP z aplikacji klienckiej. Aplikacja funkcji nie wymaga pisania kodu dla wyzwalacza. Konieczne jest tylko napisanie kodu, który jest uruchamiany w odpowiedzi na wyzwalacz. Oznacza to, że możesz skupić się na logice biznesowej zamiast poświęcać czas na tworzenie kodu do obsługi kwestii związanych z infrastrukturą, takich jak obsługa komunikatów.

Korzystanie z architektury bezserwerowej zapewnia również istotne korzyści operacyjne:

  • Nie ma potrzeby zarządzania serwerami.
  • Zasoby obliczeniowe są przydzielane dynamicznie zgodnie z potrzebami.
  • Opłaty są naliczane tylko za zasoby obliczeniowe używane do wykonywania kodu.
  • Zasoby obliczeniowe są skalowane na żądanie na podstawie ruchu.

Architektura

Na poniższym diagramie przedstawiono architekturę wysokiego poziomu aplikacji:

Diagram showing the high-level architecture of the serverless Functions application.

W jednym przepływie danych strzałki pokazują komunikaty przechodzące z pozycji Urządzenia do usługi Event Hubs i wyzwalające aplikację funkcji. Z poziomu aplikacji jedna strzałka wyświetla komunikaty utraconych wiadomości przechodzących do kolejki magazynu, a druga strzałka pokazuje zapisywanie w usłudze Azure Cosmos DB. W innym przepływie danych strzałki pokazują, że aplikacja internetowa klienta uzyskuje pliki statyczne z statycznego hostingu internetowego usługi Blob Storage za pośrednictwem sieci CDN. Inna strzałka pokazuje żądanie HTTP klienta przechodzące przez usługę API Management. W usłudze API Management jedna strzałka przedstawia wyzwalanie i odczytywanie danych z usługi Azure Cosmos DB przez aplikację funkcji. Inna strzałka pokazuje uwierzytelnianie za pomocą identyfikatora Entra firmy Microsoft. Użytkownik loguje się również do identyfikatora Entra firmy Microsoft.

Pozyskiwanie zdarzeń:

  1. Komunikaty dronów są pozyskiwane przez usługę Azure Event Hubs.
  2. Usługa Event Hubs tworzy strumień zdarzeń zawierających dane komunikatów.
  3. Te zdarzenia wyzwalają aplikację usługi Azure Functions w celu ich przetwarzania.
  4. Wyniki są przechowywane w usłudze Azure Cosmos DB.

Aplikacja internetowa:

  1. Pliki statyczne są obsługiwane przez usługę CDN z magazynu obiektów blob.
  2. Użytkownik loguje się do aplikacji internetowej przy użyciu identyfikatora Entra firmy Microsoft.
  3. Usługa Azure API Management działa jako brama, która uwidacznia punkt końcowy interfejsu API REST.
  4. Żądania HTTP od klienta wyzwalają aplikację usługi Azure Functions, która odczytuje z usługi Azure Cosmos DB i zwraca wynik.

Ta aplikacja jest oparta na dwóch architekturach referencyjnych, odpowiadających dwóm blokom funkcjonalnym opisanym powyżej:

Możesz przeczytać te artykuły, aby dowiedzieć się więcej na temat architektury wysokiego poziomu usług platformy Azure używanych w rozwiązaniu oraz zagadnień dotyczących skalowalności, zabezpieczeń i niezawodności.

Funkcja telemetrii drona

Zacznijmy od przyjrzenia się funkcji, która przetwarza komunikaty dronów z usługi Event Hubs. Funkcja jest zdefiniowana w klasie o nazwie RawTelemetryFunction:

namespace DroneTelemetryFunctionApp
{
    public class RawTelemetryFunction
    {
        private readonly ITelemetryProcessor telemetryProcessor;
        private readonly IStateChangeProcessor stateChangeProcessor;
        private readonly TelemetryClient telemetryClient;

        public RawTelemetryFunction(ITelemetryProcessor telemetryProcessor, IStateChangeProcessor stateChangeProcessor, TelemetryClient telemetryClient)
        {
            this.telemetryProcessor = telemetryProcessor;
            this.stateChangeProcessor = stateChangeProcessor;
            this.telemetryClient = telemetryClient;
        }
    }
    ...
}

Ta klasa ma kilka zależności, które są wstrzykiwane do konstruktora za pomocą wstrzykiwania zależności:

  • Interfejsy ITelemetryProcessor i IStateChangeProcessor definiują dwa obiekty pomocnika. Jak zobaczymy, te obiekty wykonują większość pracy.

  • Klasa TelemetryClient jest częścią zestawu SDK usługi Application Insights. Służy do wysyłania niestandardowych metryk aplikacji do usługi Application Insights.

Później omówimy sposób konfigurowania wstrzykiwania zależności. Na razie po prostu załóżmy, że te zależności istnieją.

Konfigurowanie wyzwalacza usługi Event Hubs

Logika w funkcji jest implementowana jako metoda asynchroniczna o nazwie RunAsync. W tym miejscu znajduje się sygnatura metody:

[FunctionName("RawTelemetryFunction")]
[StorageAccount("DeadLetterStorage")]
public async Task RunAsync(
    [EventHubTrigger("%EventHubName%", Connection = "EventHubConnection", ConsumerGroup ="%EventHubConsumerGroup%")]EventData[] messages,
    [Queue("deadletterqueue")] IAsyncCollector<DeadLetterMessage> deadLetterMessages,
    ILogger logger)
{
    // implementation goes here
}

Ta metoda przyjmuje następujące parametry:

  • messages jest tablicą komunikatów centrum zdarzeń.
  • deadLetterMessages jest kolejką usługi Azure Storage używaną do przechowywania utraconych komunikatów.
  • logging udostępnia interfejs logowania na potrzeby zapisywania dzienników aplikacji. Te dzienniki są wysyłane do usługi Azure Monitor.

Atrybut EventHubTrigger w parametrze messages konfiguruje wyzwalacz. Właściwości atrybutu określają nazwę centrum zdarzeń, parametry połączenia i grupę odbiorców. (Grupa odbiorców jest izolowanym widokiem strumienia zdarzeń usługi Event Hubs. Ta abstrakcja umożliwia wielu konsumentom tego samego centrum zdarzeń).

Zwróć uwagę na znaki procentu (%) w niektórych właściwościach atrybutu. Oznaczają one, że właściwość określa nazwę ustawienia aplikacji, a rzeczywista wartość jest pobierana z tego ustawienia aplikacji w czasie wykonywania. W przeciwnym razie, bez znaków procentu, właściwość zawiera wartość literału.

Właściwość Connection jest wyjątkiem. Ta właściwość zawsze określa nazwę ustawienia aplikacji, nigdy wartość literału, a więc znak procentu jest niepotrzebny. Przyczyną tego rozróżnienia jest fakt, że parametry połączenia są wpisem tajnym i nigdy nie powinny być ewidencjonowane w kodzie źródłowym.

Chociaż pozostałe dwie właściwości (nazwa centrum zdarzeń i grupa odbiorców) nie są danymi poufnymi, tak jak parametry połączenia, nadal lepiej umieścić je w ustawieniach aplikacji, zamiast kodować je trwale. Dzięki temu można je aktualizować bez konieczności ponownego kompilowania aplikacji.

Aby uzyskać więcej informacji na temat konfigurowania tego wyzwalacza, zobacz Azure Event Hubs bindings for Azure Functions (Powiązania usługi Azure Event Hubs dla usługi Azure Functions).

Logika przetwarzania komunikatów

Oto implementacja metody RawTelemetryFunction.RunAsync, która przetwarza partię komunikatów:

[FunctionName("RawTelemetryFunction")]
[StorageAccount("DeadLetterStorage")]
public async Task RunAsync(
    [EventHubTrigger("%EventHubName%", Connection = "EventHubConnection", ConsumerGroup ="%EventHubConsumerGroup%")]EventData[] messages,
    [Queue("deadletterqueue")] IAsyncCollector<DeadLetterMessage> deadLetterMessages,
    ILogger logger)
{
    telemetryClient.GetMetric("EventHubMessageBatchSize").TrackValue(messages.Length);

    foreach (var message in messages)
    {
        DeviceState deviceState = null;

        try
        {
            deviceState = telemetryProcessor.Deserialize(message.Body.Array, logger);

            try
            {
                await stateChangeProcessor.UpdateState(deviceState, logger);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Error updating status document", deviceState);
                await deadLetterMessages.AddAsync(new DeadLetterMessage { Exception = ex, EventData = message, DeviceState = deviceState });
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Error deserializing message", message.SystemProperties.PartitionKey, message.SystemProperties.SequenceNumber);
            await deadLetterMessages.AddAsync(new DeadLetterMessage { Exception = ex, EventData = message });
        }
    }
}

Po wywołaniu funkcji parametr messages zawiera tablicę komunikatów z centrum zdarzeń. Przetwarzanie komunikatów w partiach zwykle zapewnia lepszą wydajność niż odczytywanie po jednym komunikacie naraz. Jednak należy upewnić się, że funkcja jest odporna i bezpiecznie obsługuje błędy i wyjątki. W przeciwnym razie, jeśli funkcja zwróci nieobsługiwany wyjątek w trakcie przetwarzania partii, możesz utracić pozostałe komunikaty. Ta kwestia została omówiona bardziej szczegółowo w sekcji Obsługa błędów.

Jeśli zignorujesz obsługę wyjątków, logika przetwarzania dla każdego komunikatu jest prosta:

  1. Wywołaj metodę ITelemetryProcessor.Deserialize w celu deserializacji komunikatu zawierającego zmianę stanu urządzenia.
  2. Wywołaj metodę IStateChangeProcessor.UpdateState w celu przetworzenia tej zmiany stanu.

Przyjrzyjmy się dokładniej tym dwóm metodom, zaczynając od metody Deserialize.

Metoda Deserialize

Metoda TelemetryProcess.Deserialize przyjmuje tablicę bajtów, która zawiera ładunek komunikatu. Deserializuje ten ładunek i zwraca obiekt DeviceState reprezentujący stan drona. Stan może reprezentować aktualizację częściową, zawierającą tylko dane różnicowe od ostatniego znanego stanu. Dlatego metoda musi obsługiwać pola null w zdeserializowanym ładunku.

public class TelemetryProcessor : ITelemetryProcessor
{
    private readonly ITelemetrySerializer<DroneState> serializer;

    public TelemetryProcessor(ITelemetrySerializer<DroneState> serializer)
    {
        this.serializer = serializer;
    }

    public DeviceState Deserialize(byte[] payload, ILogger log)
    {
        DroneState restored = serializer.Deserialize(payload);

        log.LogInformation("Deserialize message for device ID {DeviceId}", restored.DeviceId);

        var deviceState = new DeviceState();
        deviceState.DeviceId = restored.DeviceId;

        if (restored.Battery != null)
        {
            deviceState.Battery = restored.Battery;
        }
        if (restored.FlightMode != null)
        {
            deviceState.FlightMode = (int)restored.FlightMode;
        }
        if (restored.Position != null)
        {
            deviceState.Latitude = restored.Position.Value.Latitude;
            deviceState.Longitude = restored.Position.Value.Longitude;
            deviceState.Altitude = restored.Position.Value.Altitude;
        }
        if (restored.Health != null)
        {
            deviceState.AccelerometerOK = restored.Health.Value.AccelerometerOK;
            deviceState.GyrometerOK = restored.Health.Value.GyrometerOK;
            deviceState.MagnetometerOK = restored.Health.Value.MagnetometerOK;
        }
        return deviceState;
    }
}

Ta metoda używa innego interfejsu pomocnika, ITelemetrySerializer<T>, do deserializacji nieprzetworzonego komunikatu. Wyniki są następnie przekształcane w model POCO, z którym łatwiej jest pracować. Ten projekt pomaga izolować logikę przetwarzania od szczegółów implementacji serializacji. Interfejs ITelemetrySerializer<T> jest zdefiniowany w bibliotece udostępnionej, która jest również używana przez symulator urządzenia w celu generowania zdarzeń symulowanego urządzenia i wysyłania ich do usługi Event Hubs.

using System;

namespace Serverless.Serialization
{
    public interface ITelemetrySerializer<T>
    {
        T Deserialize(byte[] message);

        ArraySegment<byte> Serialize(T message);
    }
}

Metoda UpdateState

Metoda StateChangeProcessor.UpdateState stosuje zmiany stanu. Ostatni znany stan każdego drona jest przechowywany jako dokument JSON w usłudze Azure Cosmos DB. Ponieważ drony wysyłają aktualizacje częściowe, aplikacja nie może po prostu zastąpić dokumentu po pobraniu aktualizacji. Zamiast tego musi pobrać poprzedni stan, scalić pola, a następnie wykonać operację upsert.

public class StateChangeProcessor : IStateChangeProcessor
{
    private IDocumentClient client;
    private readonly string cosmosDBDatabase;
    private readonly string cosmosDBCollection;

    public StateChangeProcessor(IDocumentClient client, IOptions<StateChangeProcessorOptions> options)
    {
        this.client = client;
        this.cosmosDBDatabase = options.Value.COSMOSDB_DATABASE_NAME;
        this.cosmosDBCollection = options.Value.COSMOSDB_DATABASE_COL;
    }

    public async Task<ResourceResponse<Document>> UpdateState(DeviceState source, ILogger log)
    {
        log.LogInformation("Processing change message for device ID {DeviceId}", source.DeviceId);

        DeviceState target = null;

        try
        {
            var response = await client.ReadDocumentAsync(UriFactory.CreateDocumentUri(cosmosDBDatabase, cosmosDBCollection, source.DeviceId),
                                                            new RequestOptions { PartitionKey = new PartitionKey(source.DeviceId) });

            target = (DeviceState)(dynamic)response.Resource;

            // Merge properties
            target.Battery = source.Battery ?? target.Battery;
            target.FlightMode = source.FlightMode ?? target.FlightMode;
            target.Latitude = source.Latitude ?? target.Latitude;
            target.Longitude = source.Longitude ?? target.Longitude;
            target.Altitude = source.Altitude ?? target.Altitude;
            target.AccelerometerOK = source.AccelerometerOK ?? target.AccelerometerOK;
            target.GyrometerOK = source.GyrometerOK ?? target.GyrometerOK;
            target.MagnetometerOK = source.MagnetometerOK ?? target.MagnetometerOK;
        }
        catch (DocumentClientException ex)
        {
            if (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                target = source;
            }
        }

        var collectionLink = UriFactory.CreateDocumentCollectionUri(cosmosDBDatabase, cosmosDBCollection);
        return await client.UpsertDocumentAsync(collectionLink, target);
    }
}

Ten kod używa interfejsu IDocumentClient do pobierania dokumentu z usługi Azure Cosmos DB. Jeśli dokument istnieje, nowe wartości stanu są scalane z istniejącym dokumentem. W przeciwnym razie zostaje utworzony nowy dokument. Oba przypadki są obsługiwane przez metodę UpsertDocumentAsync.

Ten kod jest zoptymalizowany pod kątem przypadku, w którym dokument już istnieje i może zostać scalony. Przy pierwszym komunikacie telemetrycznym z danego drona metoda ReadDocumentAsync zgłosi wyjątek, ponieważ nie istnieje żaden dokument dla tego drona. Po pierwszym komunikacie dokument będzie dostępny.

Zwróć uwagę, że ta klasa używa iniekcji zależności do wstrzykiwania IDocumentClient dla usługi Azure Cosmos DB i elementu IOptions<T> z ustawieniami konfiguracji. Później zobaczymy, jak skonfigurować wstrzykiwanie zależności.

Uwaga

Usługa Azure Functions obsługuje powiązanie wyjściowe dla usługi Azure Cosmos DB. To powiązanie umożliwia aplikacji funkcji pisanie dokumentów w usłudze Azure Cosmos DB bez żadnego kodu. Jednak powiązanie wyjściowe nie będzie działać w przypadku tego konkretnego scenariusza z powodu wymaganej niestandardowej logiki operacji upsert.

Obsługa błędów

Jak wspomniano wcześniej, aplikacja funkcji RawTelemetryFunction przetwarza partię komunikatów w pętli. Oznacza to, że funkcja musi bezpiecznie obsługiwać wszelkie wyjątki i kontynuować przetwarzanie pozostałej części partii. W przeciwnym razie komunikaty mogłyby zostać porzucone.

W przypadku napotkania wyjątku podczas przetwarzania komunikatu funkcja umieszcza komunikat w kolejce utraconych komunikatów:

catch (Exception ex)
{
    logger.LogError(ex, "Error deserializing message", message.SystemProperties.PartitionKey, message.SystemProperties.SequenceNumber);
    await deadLetterMessages.AddAsync(new DeadLetterMessage { Exception = ex, EventData = message });
}

Kolejka utraconych komunikatów jest definiowana za pomocą powiązania wyjściowego z kolejką magazynu:

[FunctionName("RawTelemetryFunction")]
[StorageAccount("DeadLetterStorage")]  // App setting that holds the connection string
public async Task RunAsync(
    [EventHubTrigger("%EventHubName%", Connection = "EventHubConnection", ConsumerGroup ="%EventHubConsumerGroup%")]EventData[] messages,
    [Queue("deadletterqueue")] IAsyncCollector<DeadLetterMessage> deadLetterMessages,  // output binding
    ILogger logger)

Tutaj atrybut Queue określa powiązanie wyjściowe, a atrybut StorageAccount określa nazwę ustawienia aplikacji, które zawiera parametry połączenia dla konta magazynu.

Porada dotycząca wdrażania: w szablonie usługi Resource Manager, który tworzy konto magazynu, możesz automatycznie wypełnić ustawienie aplikacji parametry połączenia. Sztuczką jest użycie funkcji listKeys .

Oto sekcja szablonu, która tworzy konto magazynu dla kolejki:

    {
        "name": "[variables('droneTelemetryDeadLetterStorageQueueAccountName')]",
        "type": "Microsoft.Storage/storageAccounts",
        "location": "[resourceGroup().location]",
        "apiVersion": "2017-10-01",
        "sku": {
            "name": "[parameters('storageAccountType')]"
        },

Oto sekcja szablonu, która tworzy aplikację funkcji:


    {
        "apiVersion": "2015-08-01",
        "type": "Microsoft.Web/sites",
        "name": "[variables('droneTelemetryFunctionAppName')]",
        "location": "[resourceGroup().location]",
        "tags": {
            "displayName": "Drone Telemetry Function App"
        },
        "kind": "functionapp",
        "dependsOn": [
            "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
            ...
        ],
        "properties": {
            "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
            "siteConfig": {
                "appSettings": [
                    {
                        "name": "DeadLetterStorage",
                        "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('droneTelemetryDeadLetterStorageQueueAccountName'), ';AccountKey=', listKeys(variables('droneTelemetryDeadLetterStorageQueueAccountId'),'2015-05-01-preview').key1)]"
                    },
                    ...

Definiuje ona ustawienie aplikacji o nazwie DeadLetterStorage, którego wartość jest wypełniana przy użyciu funkcji listKeys. Ważne jest, aby zasób aplikacji funkcji był zależny od zasobu konta magazynu (zobacz element dependsOn). Gwarantuje to, że najpierw zostanie utworzone konto magazynu i parametry połączenia będą dostępne.

Konfigurowanie wstrzykiwania zależności

Poniższy kod konfiguruje wstrzykiwanie zależności dla funkcji RawTelemetryFunction:

[assembly: FunctionsStartup(typeof(DroneTelemetryFunctionApp.Startup))]

namespace DroneTelemetryFunctionApp
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddOptions<StateChangeProcessorOptions>()
                .Configure<IConfiguration>((configSection, configuration) =>
                {
                    configuration.Bind(configSection);
                });

            builder.Services.AddTransient<ITelemetrySerializer<DroneState>, TelemetrySerializer<DroneState>>();
            builder.Services.AddTransient<ITelemetryProcessor, TelemetryProcessor>();
            builder.Services.AddTransient<IStateChangeProcessor, StateChangeProcessor>();

            builder.Services.AddSingleton<IDocumentClient>(ctx => {
                var config = ctx.GetService<IConfiguration>();
                var cosmosDBEndpoint = config.GetValue<string>("CosmosDBEndpoint");
                var cosmosDBKey = config.GetValue<string>("CosmosDBKey");
                return new DocumentClient(new Uri(cosmosDBEndpoint), cosmosDBKey);
            });
        }
    }
}

Usługa Azure Functions napisana dla platformy .NET może używać struktury wstrzykiwania zależności platformy ASP.NET Core. Podstawowa koncepcja polega na tym, że deklarujesz metodę startową dla zestawu. Ta metoda przyjmuje interfejs IFunctionsHostBuilder, który służy do deklarowania zależności dla wstrzykiwania zależności. W tym celu należy wywołać metodę Add* dla obiektu Services. Podczas dodawania zależności określasz jej okres istnienia:

  • Obiekty przejściowe są tworzone za każdym razem, kiedy są żądane.
  • Obiekty w zakresie są tworzone raz dla danego wykonania funkcji.
  • Obiekty pojedyncze są ponownie używane we wszystkich wykonaniach funkcji w okresie istnienia hosta funkcji.

W tym przykładzie obiekty TelemetryProcessor i StateChangeProcessor są deklarowane jako przejściowe. Jest to odpowiednie w przypadku lekkich, bezstanowych usług. Natomiast klasa DocumentClient powinna być pojedyncza w celu uzyskania najlepszej wydajności. Aby uzyskać więcej informacji, zobacz porady dotyczące wydajności usługi Azure Cosmos DB i platformy .NET.

Jeśli wrócisz do kodu RawTelemetryFunction, zobaczysz tam inną zależność, która nie pojawia się w kodzie konfiguracji wstrzykiwania zależności, a mianowicie klasę TelemetryClient, która służy do rejestrowania metryk aplikacji. Środowisko uruchomieniowe usługi Functions automatycznie rejestruje tę klasę w kontenerze wstrzykiwania zależności, więc nie trzeba jej jawnie rejestrować.

Aby uzyskać więcej informacji na temat wstrzykiwania zależności w usłudze Azure Functions, zobacz następujące artykuły:

Przekazywanie ustawień konfiguracji we wstrzykiwaniu zależności

Czasami obiekt musi zostać zainicjowany z pewnymi wartościami konfiguracji. Ogólnie rzecz biorąc, te ustawienia powinny pochodzić z ustawień aplikacji lub (w przypadku wpisów tajnych) z usługi Azure Key Vault.

Istnieją dwa przykłady w tej aplikacji. DocumentClient Najpierw klasa przyjmuje punkt końcowy i klucz usługi Azure Cosmos DB. Dla tego obiektu aplikacja rejestruje funkcję lambda, która zostanie wywołana przez kontener wstrzykiwania zależności. Ta funkcja lambda używa interfejsu IConfiguration w celu odczytania wartości konfiguracji:

builder.Services.AddSingleton<IDocumentClient>(ctx => {
    var config = ctx.GetService<IConfiguration>();
    var cosmosDBEndpoint = config.GetValue<string>("CosmosDBEndpoint");
    var cosmosDBKey = config.GetValue<string>("CosmosDBKey");
    return new DocumentClient(new Uri(cosmosDBEndpoint), cosmosDBKey);
});

Drugim przykładem jest klasa StateChangeProcessor. W przypadku tego obiektu używamy podejścia o nazwie wzorzec opcji. Oto jak działa ta polityka:

  1. Zdefiniuj klasę T zawierającą ustawienia konfiguracji. W takim przypadku nazwa bazy danych i nazwa kolekcji usługi Azure Cosmos DB.

    public class StateChangeProcessorOptions
    {
        public string COSMOSDB_DATABASE_NAME { get; set; }
        public string COSMOSDB_DATABASE_COL { get; set; }
    }
    
  2. Dodaj klasę T jako klasę opcji dla wstrzykiwania zależności.

    builder.Services.AddOptions<StateChangeProcessorOptions>()
        .Configure<IConfiguration>((configSection, configuration) =>
        {
            configuration.Bind(configSection);
        });
    
  3. W konstruktorze klasy, która jest konfigurowana, uwzględnij parametr IOptions<T>.

    public StateChangeProcessor(IDocumentClient client, IOptions<StateChangeProcessorOptions> options)
    

System wstrzykiwania zależności automatycznie wypełni klasę opcji wartościami konfiguracji i przekaże ją do konstruktora.

To podejście ma kilka zalet:

  • Oddzielenie klasy od źródła wartości konfiguracji.
  • Pozwala łatwo skonfigurować różne źródła konfiguracji, takie jak zmienne środowiskowe lub pliki konfiguracji JSON.
  • Uproszczenie testów jednostkowych.
  • Korzystanie z silnie typizowanej klasy opcji, która jest mniej podatna na błędy niż zwykłe przekazywanie wartości skalarnych.

Funkcja GetStatus

Inna aplikacja usługi Functions w tym rozwiązaniu implementuje prosty interfejs API REST w celu pobierania ostatniego znanego stanu drona. Ta funkcja jest zdefiniowana w klasie o nazwie GetStatusFunction. Oto pełny kod tej funkcji:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Security.Claims;
using System.Threading.Tasks;

namespace DroneStatusFunctionApp
{
    public static class GetStatusFunction
    {
        public const string GetDeviceStatusRoleName = "GetStatus";

        [FunctionName("GetStatusFunction")]
        public static IActionResult Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)]HttpRequest req,
            [CosmosDB(
                databaseName: "%COSMOSDB_DATABASE_NAME%",
                collectionName: "%COSMOSDB_DATABASE_COL%",
                ConnectionStringSetting = "COSMOSDB_CONNECTION_STRING",
                Id = "{Query.deviceId}",
                PartitionKey = "{Query.deviceId}")] dynamic deviceStatus,
            ClaimsPrincipal principal,
            ILogger log)
        {
            log.LogInformation("Processing GetStatus request.");

            if (!principal.IsAuthorizedByRoles(new[] { GetDeviceStatusRoleName }, log))
            {
                return new UnauthorizedResult();
            }

            string deviceId = req.Query["deviceId"];
            if (deviceId == null)
            {
                return new BadRequestObjectResult("Missing DeviceId");
            }

            if (deviceStatus == null)
            {
                return new NotFoundResult();
            }
            else
            {
                return new OkObjectResult(deviceStatus);
            }
        }
    }
}

Ta funkcja używa wyzwalacza HTTP do przetworzenia żądania HTTP GET. Funkcja używa powiązania wejściowego usługi Azure Cosmos DB, aby pobrać żądany dokument. Należy pamiętać, że to powiązanie będzie uruchamiane przed wykonaniem logiki autoryzacji wewnątrz funkcji. Jeśli nieautoryzowany użytkownik zażąda dokumentu, powiązanie funkcji spowoduje pobranie dokumentu. Następnie kod autoryzacji zwróci błąd 401, dzięki czemu użytkownik nie zobaczy dokumentu. To, czy takie zachowanie jest dopuszczalne, zależy od wymagań organizacji. Na przykład to podejście może utrudnić inspekcję dostępu do danych poufnych.

Uwierzytelnianie i autoryzacja

Aplikacja internetowa używa identyfikatora Entra firmy Microsoft do uwierzytelniania użytkowników. Ponieważ aplikacja jest aplikacją jednostronicową uruchamianą w przeglądarce, przepływ przyznawania niejawnego jest odpowiednią opcją:

  1. Aplikacja internetowa przekierowuje użytkownika do dostawcy tożsamości (w tym przypadku identyfikator Firmy Microsoft Entra).
  2. Użytkownik wprowadza swoje poświadczenia.
  3. Dostawca tożsamości przekierowuje go z powrotem do aplikacji internetowej przy użyciu tokenu dostępu.
  4. Aplikacja internetowa wysyła żądanie do internetowego interfejsu API i dołącza token dostępu w nagłówku autoryzacji.

Implicit flow diagram

Aplikację funkcji można skonfigurować do uwierzytelniania użytkowników bez żadnego kodu. Aby uzyskać więcej informacji, zobacz Uwierzytelnianie i autoryzacja w usłudze Azure App Service.

Z drugiej strony autoryzacja zazwyczaj wymaga logiki biznesowej. Identyfikator Entra firmy Microsoft obsługuje uwierzytelnianie oparte na oświadczeniach. W tym modelu tożsamość użytkownika jest reprezentowana jako zestaw oświadczeń pochodzących od dostawcy tożsamości. Oświadczenie może być dowolnym elementem informacji o użytkowniku, takim jak jego nazwa lub adres e-mail.

Token dostępu zawiera podzestaw oświadczeń użytkownika. Wśród nich znajdują się dowolne role aplikacji, do których użytkownik jest przypisany.

Parametr principal funkcji jest obiektem ClaimsPrincipal, który zawiera oświadczenia z tokenu dostępu. Każde oświadczenie jest parą klucz-wartość typu oświadczenia i wartości oświadczenia. Aplikacja używa tych elementów do autoryzowania żądania.

Poniższa metoda rozszerzenia sprawdza, czy obiekt ClaimsPrincipal zawiera zestaw ról. Zwraca wartość false w przypadku braku dowolnej z określonych ról. Jeśli ta metoda zwróci wartość false, funkcja zwróci HTTP 401 (Brak autoryzacji).

namespace DroneStatusFunctionApp
{
    public static class ClaimsPrincipalAuthorizationExtensions
    {
        public static bool IsAuthorizedByRoles(
            this ClaimsPrincipal principal,
            string[] roles,
            ILogger log)
        {
            var principalRoles = new HashSet<string>(principal.Claims.Where(kvp => kvp.Type == "roles").Select(kvp => kvp.Value));
            var missingRoles = roles.Where(r => !principalRoles.Contains(r)).ToArray();
            if (missingRoles.Length > 0)
            {
                log.LogWarning("The principal does not have the required {roles}", string.Join(", ", missingRoles));
                return false;
            }

            return true;
        }
    }
}

Aby uzyskać więcej informacji na temat uwierzytelniania i autoryzacji w tej aplikacji, zobacz sekcję Zagadnienia związane z zabezpieczeniami architektury referencyjnej.

Następne kroki

Gdy dowiesz się, jak działa to rozwiązanie referencyjne, poznaj najlepsze rozwiązania i zalecenia dotyczące podobnych rozwiązań.

Usługa Azure Functions to tylko jedna opcja obliczeniowa platformy Azure. Aby uzyskać pomoc dotyczącą wybierania technologii obliczeniowej, zobacz Wybieranie usługi obliczeniowej platformy Azure dla aplikacji.