Exemplarische Vorgehensweise mit Code: Serverlose Anwendung mit Azure Functions

Event Hubs
Functions

Serverlose Modelle abstrahieren Code aus der zugrunde liegenden Compute-Infrastruktur, sodass sich Entwickler ohne umfassendes Setup auf die Geschäftslogik konzentrieren können. Serverloser Code senkt Kosten, da Sie nur für die Ressourcen und die Dauer der Codeausführung bezahlen müssen.

Das serverlose ereignisgesteuerte Modell ist für Situationen geeignet, in denen ein bestimmtes Ereignis eine definierte Aktion auslöst. Der Empfang einer eingehenden Gerätenachricht löst beispielsweise die Speicherung für die spätere Verwendung aus, oder ein Datenbankupdate löst eine weitere Verarbeitung aus.

Um Sie bei der Erkundung serverloser Azure-Technologien zu unterstützen, hat Microsoft eine serverlose Anwendung entwickelt und getestet, die Azure Functions verwendet. In diesem Artikel werden Sie schrittweise durch den Code für die serverlose Functions-Lösung geführt. Außerdem werden Entwurfsentscheidungen, Implementierungsdetails und einige der möglicherweise auftretenden Probleme beschrieben.

Erkunden der Lösung

Die zweiteilige Lösung beschreibt ein hypothetisches Auslieferungssystem mit Drohnen. Drohnen senden den Flugstatus an die Cloud, wo diese Nachrichten zur späteren Verwendung gespeichert werden. Mit einer Web-App können Benutzern die Nachrichten abrufen und den aktuellen Status der Geräte erfahren.

Sie können den Code für diese Lösung von GitHub herunterladen.

Diese exemplarische Vorgehensweise setzt ein grundlegendes Maß an Vertrautheit mit den folgenden Technologien voraus:

Sie müssen kein Experte für Azure Functions oder Event Hubs sein, jedoch sollten Sie mit den jeweiligen Features allgemein vertraut sein. Hier einige geeignete Ressourcen für Ihren Einstieg:

Das Szenario

Diagram of the functional blocks

Fabrikam betreibt eine Flotte von Drohnen für einen Drohnenlieferdienst. Die Anwendung besteht aus zwei Hauptfunktionsbereichen:

  • Ereigniserfassung. Während des Flugs senden Drohnen Statusmeldungen an einen Cloudendpunkt. Die Anwendung erfasst und verarbeitet diese Nachrichten und schreibt die Ergebnisse in eine Back-End-Datenbank (Cosmos DB). Die Geräte senden Nachrichten im Protocol Buffers-Format (protobuf). protobuf ist ein effizientes und selbstbeschreibendes Serialisierungsformat.

    Diese Nachrichten enthalten Teilupdates. In einem festen Intervall sendet jede Drohne eine „Keyframe“-Nachricht, die alle Statusfelder enthält. Zwischen Keyframes enthalten die Statusmeldungen nur Felder, die sich seit der letzten Nachricht geändert haben. Dieses Verhalten ist typisch für viele IoT-Geräte, die mit Bandbreite und Energie sparsam umgehen müssen.

  • Web-App. Eine Webanwendung ermöglicht Benutzern, ein Gerät nachzuschlagen und den letzten bekannten Status des Geräts abzufragen. Benutzer müssen sich bei der Anwendung anmelden und mithilfe von Azure Active Directory (Azure AD) authentifizieren. Die Anwendung lässt nur Anforderungen von Benutzern zu, die zum Zugriff auf die App berechtigt sind.

Hier sehen Sie einen Screenshot der Webanwendung, der das Ergebnis einer Abfrage zeigt:

Screenshot of client app

Entwerfen der Anwendung

Fabrikam hat sich entschieden, zur Implementierung der Geschäftslogik der Anwendung Azure Functions einzusetzen. Azure Functions ist ein Beispiel für „Functions as a Service“ (FaaS). Bei diesem Computingmodell ist eine Funktion ein Teil des Codes, der in der Cloud bereitgestellt und in einer Hostingumgebung ausgeführt wird. In dieser Hostingumgebung werden die Server, auf denen der Code ausgeführt wird, vollständig abstrahiert.

Was spricht für einen serverlosen Ansatz?

Eine serverlose Architektur mit Azure Functions ist ein Beispiel einer ereignisgesteuerten Architektur. Der Funktionscode wird durch ein Ereignis ausgelöst, das in diesem Fall außerhalb der Funktion liegt, entweder eine von einer Drohne stammende Nachricht oder eine von einer Clientanwendung stammende HTTP-Anforderung. Bei einer Funktions-App müssen Sie keinen Code für den Trigger schreiben. Sie schreiben lediglich den Code, der als Reaktion auf den Trigger ausgeführt wird. Das bedeutet, dass Sie sich auf Ihre Geschäftslogik konzentrieren können, anstatt viel Code zu schreiben, um Infrastrukturaspekte wie Messaging in den Griff zu bekommen.

Der Einsatz einer serverlosen Architektur bringt auch betriebliche Vorteile:

  • Es müssen keine Server verwaltet werden.
  • Computeressourcen werden je nach Anforderungen dynamisch zugeteilt.
  • Ihnen werden nur die Computeressourcen berechnet, die zum Ausführen des Codes verwendet werden.
  • Die Computeressourcen lassen sich basierend auf dem Datenverkehr je nach Bedarf skalieren.

Aufbau

Das folgende Diagramm zeigt die allgemeine Architektur der Anwendung:

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

In einem Datenfluss zeigen Pfeile an, dass die Nachrichten von Geräten zu Event Hubs weitergeleitet werden und die Funktions-App auslösen. Ein Pfeil von der App zeigt unzustellbare Nachrichten, die in eine Speicherwarteschlange weitergeleitet werden, und ein weiterer Pfeil zeigt das Schreiben in Azure Cosmos DB. In einem anderen Datenfluss zeigen Pfeile, dass die Client-Web-App statische Dateien vom statischen Webhosting in Blob Storage über ein CDN erhält. Ein weiterer Pfeil zeigt die HTTP-Anforderung des Clients, die API Management durchläuft. In API Management zeigt ein Pfeil die Funktions-App, die ausgelöst wird und Daten aus Azure Cosmos DB liest. Ein weiterer Pfeil zeigt die Authentifizierung über Azure AD. Außerdem meldet sich ein Benutzer bei Azure AD an.

Ereigniserfassung:

  1. Nachrichten von Drohnen werden von Azure Event Hubs erfasst.
  2. Event Hubs erzeugt einen Ereignisdatenstrom, der die Nachrichtendaten enthält.
  3. Diese Ereignisse lösen eine Azure Functions-App aus, mit der sie verarbeitet werden.
  4. Die Ergebnisse werden in Cosmos DB gespeichert.

Web-App:

  1. Statische Dateien werden vom CDN aus Blob Storage bereitgestellt.
  2. Ein Benutzer meldet sich mithilfe von Azure AD bei der Web-App an.
  3. Azure API Management fungiert als Gateway, das einen REST-API-Endpunkt verfügbar macht.
  4. HTTP-Anforderungen vom Client lösen eine Azure Functions-App aus, die Daten aus Cosmos DB liest und das Ergebnis zurückgibt.

Diese Anwendung basiert auf zwei Referenzarchitekturen, die den beiden zuvor beschriebenen Funktionsblöcken entsprechen:

Sie können in diesen Artikeln mehr über die allgemeine Architektur, die in der Lösung verwendeten Azure-Dienste und Aspekte zu Skalierbarkeit, Sicherheit und Zuverlässigkeit erfahren.

Telemetriefunktion von Drohnen

Beginnen wir mit der Funktion, die von Event Hubs stammende Drohnennachrichten verarbeitet. Die Funktion ist in einer Klasse mit dem Namen RawTelemetryFunction definiert:

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

Diese Klasse hat mehrere Abhängigkeiten, die per Abhängigkeitsinjektion in den Konstruktor injiziert werden:

  • Die Schnittstellen ITelemetryProcessor und IStateChangeProcessor definieren zwei Hilfsobjekte. Wie wir sehen werden, verrichten diese Objekte die meiste Arbeit.

  • Das TelemetryClient-Element ist Teil des Application Insights SDK. Es wird zum Senden benutzerdefinierter Metriken an Application Insights verwendet.

Später sehen wir uns an, wie die Abhängigkeitsinjektion konfiguriert wird. Im Moment gehen wir einfach davon aus, dass diese Abhängigkeiten existieren.

Konfigurieren des Event Hubs-Triggers

Die Logik in der Funktion ist als asynchrone Methode mit dem Namen RunAsync implementiert. Hier die Signatur der Methode:

[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
}

Die Methode verwendet die folgenden Parameter:

  • messages ist ein Array von Event Hub-Nachrichten.
  • deadLetterMessages ist eine Azure Storage-Warteschlange zum Speichern unzustellbarer Nachrichten.
  • logging bietet eine Protokollierungsschnittstelle für das Schreiben von Anwendungsprotokollen. Diese Protokolle werden an Azure Monitor gesendet.

Mit dem Attribut EventHubTrigger für den Parameter messages wird der Trigger konfiguriert. Die Eigenschaften des Attributs geben einen Event Hub-Namen, eine Verbindungszeichenfolge und eine Consumergruppe an. (Eine Consumergruppe ist eine isolierte Ansicht des Azure Event Hubs-Ereignisdatenstroms. Diese Abstraktion ermöglicht mehreren Consumern desselben Ereignishubs.)

Beachten Sie die Prozentzeichen (%) in einigen der Eigenschaften des Attributs. Sie bedeuten, dass die Eigenschaft den Namen einer App-Einstellung angibt und der tatsächliche Wert zur Laufzeit dieser App-Einstellung entnommen wird. Ohne Prozentzeichen gibt die Eigenschaft den Literalwert an.

Die Connection-Eigenschaft ist eine Ausnahme. Diese Eigenschaft gibt stets den Namen der App-Einstellung und keinen Literalwert an, weshalb das Prozentzeichen nicht benötigt wird. Der Grund für diese Unterscheidung ist, dass eine Verbindungszeichenfolge geheim ist und niemals in den Quellcode eingecheckt werden sollte.

Obwohl die beiden anderen Eigenschaften (Event Hub-Name und Consumergruppe) keine sensiblen Daten wie eine Verbindungszeichenfolge darstellen, ist es immer noch besser, sie in die App-Einstellungen zu integrieren, statt sie hart zu codieren. Auf diese Weise können sie aktualisiert werden, ohne die App neu zu kompilieren.

Weitere Informationen zum Konfigurieren dieses Triggers finden Sie unter Azure Event Hubs-Bindungen für Azure Functions.

Nachrichtenverarbeitungslogik

Hier ist die Implementierung der RawTelemetryFunction.RunAsync-Methode, die einen Batch von Nachrichten verarbeitet:

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

Wenn die Funktion aufgerufen wird, enthält der Parameter messages ein Array von Nachrichten, die von der Event Hub-Instanz stammen. Die Batchverarbeitung von Nachrichten führt in der Regel zu einer besseren Leistung als das sequenzielle Lesen von Nachrichten. Sie müssen jedoch sicherstellen, dass die Funktion zuverlässig ist und Fehler und Ausnahmen ordnungsgemäß behandelt werden. Andernfalls können, wenn die Funktion mitten in einem Batch eine unbehandelte Ausnahme auslöst, die restlichen Nachrichten verloren gehen. Dieser Aspekt wird im Abschnitt Fehlerbehandlung ausführlicher erörtert.

Wenn Sie jedoch die Ausnahmebehandlung ignorieren, ist die Verarbeitungslogik für jede Nachricht einfach:

  1. Rufen Sie ITelemetryProcessor.Deserialize auf, um die Nachricht zu deserialisieren, die eine Änderung des Gerätezustands enthält.
  2. Rufen Sie IStateChangeProcessor.UpdateState auf, um die Zustandsänderung zu verarbeiten.

Lassen Sie uns diese beiden Methoden näher betrachten, wobei wir mit der Deserialize-Methode beginnen.

Deserialisierungsmethode

Die TelemetryProcess.Deserialize-Methode verwendet ein Bytearray, das die Nachrichtennutzlast enthält. Sie deserialisiert diese Nutzlast und gibt ein DeviceState-Objekt zurück, das den Zustand einer Drohne darstellt. Der Zustand kann ein Teilupdate darstellen, das nur das Delta des letzten bekannten Zustandes enthält. Daher muss die Methode Felder des Typs null in der deserialisierten Nutzlast behandeln.

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

Diese Methode verwendet mit ITelemetrySerializer<T> eine weitere Hilfsschnittstelle, um die Rohnachricht zu deserialisieren. Die Ergebnisse werden dann in ein POCO-Modell umgewandelt, mit dem sich leichter arbeiten lässt. Dieser Ansatz hilft, die Verarbeitungslogik von den Details der Implementierung der Serialisierung zu trennen. Die ITelemetrySerializer<T>-Schnittstelle ist in einer freigegebenen Bibliothek definiert, die auch vom Gerätesimulator verwendet wird, um simulierte Geräteereignisse zu erzeugen und an Event Hubs zu senden.

using System;

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

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

UpdateState-Methode

Die StateChangeProcessor.UpdateState-Methode wendet die Zustandsänderungen an. Der letzte bekannte Zustand jeder Drohne wird als JSON-Dokument in Cosmos DB gespeichert. Da die Drohnen Teilupdates senden, kann die Anwendung das Dokument nicht einfach überschreiben, sobald sie ein Update erhält. Stattdessen muss es den vorherigen Zustand abrufen, die Felder zusammenführen und dann einen Upsert-Vorgang durchführen.

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

Dieser Code verwendet die IDocumentClient-Schnittstelle, um ein Dokument aus Cosmos DB abzurufen. Wenn das Dokument vorhanden ist, werden die neuen Zustandswerte mit dem bestehenden Dokument zusammengeführt. Andernfalls wird ein neues Dokument erstellt. Beide Fälle werden von der UpsertDocumentAsync-Methode verarbeitet.

Dieser Code ist für den Fall optimiert, dass das Dokument bereits vorhanden ist und zusammengeführt werden kann. Bei der ersten Telemetrienachricht einer bestimmten Drohne löst die ReadDocumentAsync-Methode eine Ausnahme aus, da es für diese Drohne kein Dokument gibt. Nach der ersten Nachricht ist das Dokument verfügbar.

Beachten Sie, dass diese Klasse die Abhängigkeitsinjektion verwendet, um den IDocumentClient für Cosmos DB und IOptions<T> mit Konfigurationseinstellungen zu injizieren. Wir zeigen später, wie die Abhängigkeitsinjektion eingerichtet wird.

Hinweis

Azure Functions bietet eine Ausgabebindung für Cosmos DB. Diese Bindung ermöglicht der Funktions-App, ohne Code Dokumente in Cosmos DB zu schreiben. Allerdings funktioniert die Ausgabebindung für dieses spezielle Szenario aufgrund der benötigten benutzerdefinierten Upsert-Logik nicht.

Fehlerbehandlung

Wie bereits erwähnt, verarbeitet die Funktions-App RawTelemetryFunction einen Batch von Nachrichten in einer Schleife. Dies bedeutet, dass die Funktion alle Ausnahmen ordnungsgemäß behandeln und den Rest des Batches weiterverarbeiten muss. Andernfalls können Nachrichten verloren gehen.

Wenn beim Verarbeiten einer Nachricht eine Ausnahme eintritt, legt die Funktion die Nachricht in einer Warteschlange für unzustellbare Nachrichten ab:

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

Die Warteschlange für unzustellbare Nachrichten wird durch eine Ausgabebindung an eine Speicherwarteschlange definiert:

[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)

Hier gibt das Attribut Queue die Ausgabebindung an. Das Attribut StorageAccount gibt den Namen einer App-Einstellung an, die die Verbindungszeichenfolge für das Speicherkonto enthält.

Bereitstellungstipp: In der Resource Manager-Vorlage für das Erstellen des Speicherkontos können Sie eine App-Einstellung automatisch mit der Verbindungszeichenfolge auffüllen. Der Trick dabei ist die Verwendung der listKeys-Funktion.

Hier ist der Abschnitt der Vorlage, in dem das Speicherkonto für die Warteschlange angelegt wird:

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

Hier ist der Abschnitt der Vorlage, in dem die Funktions-App erstellt wird.


    {
        "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)]"
                    },
                    ...

Hiermit wird eine App-Einstellung namens DeadLetterStorage definiert, deren Wert mithilfe der listKeys-Funktion aufgefüllt wird. Es ist wichtig, die Funktions-App-Ressource von der Speicherkontenressource abhängig zu machen (siehe das dependsOn-Element). Dadurch wird sichergestellt, dass das Speicherkonto zuerst erstellt wird und die Verbindungszeichenfolge zur Verfügung steht.

Einrichten der Abhängigkeitsinjektion

Mit dem folgenden Code wird die Abhängigkeitsinjektion für die Funktion RawTelemetryFunction eingerichtet:

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

Für .NET geschriebene Azure Functions-Instanzen können das ASP.NET Core-Framework für Abhängigkeitsinjektion verwenden. Die Grundidee ist, dass Sie für Ihre Assembly eine Startmethode deklarieren. Die Methode verwendet eine IFunctionsHostBuilder-Schnittstelle, die zum Deklarieren von Abhängigkeiten für die Abhängigkeitsinjektion verwendet wird. Rufen Sie dazu für das Services-Objekt die Add*-Methode auf. Wenn Sie eine Abhängigkeit hinzufügen, geben Sie ihre Lebensdauer an:

  • Kurzlebige Objekte werden jedes Mal erstellt, wenn sie angefordert werden.
  • Bereichsbezogene Objekte werden einmal pro Funktionsausführung erstellt.
  • Singleton-Objekte werden für Ausführungen der Funktion innerhalb der Lebensdauer des Funktionshosts wiederverwendet.

In diesem Beispiel die Objekte TelemetryProcessor und StateChangeProcessor als kurzlebig deklariert. Dies ist für einfache, zustandslose Dienste sinnvoll. Die DocumentClient-Klasse hingegen sollte zum Erzielen der besten Leistung ein Singleton sein. Weitere Informationen finden Sie im Artikel zu den Leistungstipps für Azure Cosmos DB und .NET.

Wenn Sie sich erneut mit dem Code für die Funktion RawTelemetryFunction befassen, sehen Sie dort eine andere Abhängigkeit, die nicht im Code zur Einrichtung der Abhängigkeitsinjektion vorkommt, nämlich die TelemetryClient-Klasse, die zur Protokollierung von Anwendungsmetriken verwendet wird. Die Azure Functions-Laufzeit registriert diese Klasse automatisch im Container für die Abhängigkeitsinjektion, damit Sie sie nicht explizit registrieren müssen.

Weitere Informationen zur Abhängigkeitsinjektion in Azure Functions finden Sie in den folgenden Artikeln:

Übergeben von Konfigurationseinstellungen bei der Abhängigkeitsinjektion

Mitunter muss ein Objekt mit einigen Konfigurationswerten initialisiert werden. Im Allgemeinen sollten diese Einstellungen aus den App-Einstellungen oder (im Fall von Geheimnissen) aus Azure Key Vault stammen.

Es gibt in dieser Anwendung zwei Beispiele. Zuerst verwendet die DocumentClient-Klasse einen Cosmos DB-Dienstendpunkt und -Schlüssel. Für dieses Objekt registriert die Anwendung einen Lambda-Ausdruck, der vom Container für die Abhängigkeitsinjektion aufgerufen wird. Dieser Lambda-Ausdruck verwendet die IConfiguration-Schnittstelle, um die Konfigurationswerte zu lesen:

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

Das zweite Beispiel ist die StateChangeProcessor-Klasse. Für dieses Objekt verwenden wir einen Ansatz, der Optionsmuster genannt wird. Funktionsweise:

  1. Definieren Sie eine T-Klasse, die Ihre Konfigurationseinstellungen enthält. Dies sind in diesem Fall der Name der Cosmos DB-Datenbank und der Name der Sammlung.

    public class StateChangeProcessorOptions
    {
        public string COSMOSDB_DATABASE_NAME { get; set; }
        public string COSMOSDB_DATABASE_COL { get; set; }
    }
    
  2. Fügen Sie die T-Klasse als Optionsklasse für die Abhängigkeitsinjektion hinzu.

    builder.Services.AddOptions<StateChangeProcessorOptions>()
        .Configure<IConfiguration>((configSection, configuration) =>
        {
            configuration.Bind(configSection);
        });
    
  3. Fügen Sie im Konstruktor der zu konfigurierenden Klasse einen IOptions<T>-Parameter hinzu.

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

Das System für die Abhängigkeitsinjektion füllt die Optionsklasse mit Konfigurationswerten auf und übergibt diese an den Konstruktor.

Dieser Ansatz hat mehrere Vorteile:

  • Entkopplung der Klasse von der Quelle der Konfigurationswerte.
  • Einfaches Einrichten verschiedener Konfigurationsquellen, z.B. Umgebungsvariablen oder JSON-Konfigurationsdateien.
  • Vereinfachte Komponententests.
  • Verwenden einer stark typisierten Optionsklasse, was weniger fehleranfällig als das Übergeben von Skalarwerten ist.

GetStatus-Funktion

Die andere Azure Functions-App in dieser Lösung implementiert eine einfache REST-API, um den letzten bekannten Status einer Drohne abzurufen. Diese Funktion ist in einer Klasse mit dem Namen GetStatusFunction definiert. Hier der vollständige Code der Funktion:

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

Diese Funktion verwendet einen HTTP-Trigger, um eine HTTP GET-Anforderung zu verarbeiten. Die Funktion verwendet eine Cosmos DB-Eingabebindung, um das angeforderte Dokument abzurufen. Ein Aspekt ist, dass diese Bindung ausgeführt wird, bevor die Berechtigungslogik innerhalb der Funktion ausgeführt wird. Wenn ein nicht autorisierter Benutzer ein Dokument anfordert, ruft die Funktionsbindung das Dokument trotzdem ab. Anschließend gibt der Autorisierungscode den Code 401 zurück, sodass der Benutzer das Dokument nicht sehen kann. Ob dieses Verhalten akzeptabel ist, hängt von Ihren Anforderungen ab. Dieser Ansatz kann es beispielsweise erschweren, den Datenzugriff auf sensible Daten zu überprüfen.

Authentifizierung und Autorisierung

Die Web-App verwendet Azure AD, um Benutzer zu authentifizieren. Da die App eine Single-Page-Webanwendung ist, die im Browser ausgeführt wird, ist der Flow der impliziten Gewährung sinnvoll:

  1. Die Web-App leitet den Benutzer zum Identitätsanbieter (in diesem Fall Azure AD) um.
  2. Der Benutzer gibt seine Anmeldeinformation ein.
  3. Der Identitätsanbieter nimmt eine Umleitung mit einem Zugriffstoken zurück zur Web-App vor.
  4. Die Web-App sendet eine Anforderung an die Web-API und fügt das Zugriffstoken in den Autorisierungsheader ein.

Implicit flow diagram

Eine Azure Functions-Anwendung kann so konfiguriert werden, dass Benutzer ohne Code authentifiziert werden. Weitere Informationen finden Sie unter Authentifizierung und Autorisierung in Azure App Service.

Die Autorisierung hingegen erfordert in der Regel eine gewisse Geschäftslogik. Azure AD unterstützt die anspruchsbasierte Authentifizierung. In diesem Modell wird die Identität eines Benutzers als eine Reihe von Ansprüchen dargestellt, die vom Identitätsanbieter stammen. Ein Anspruch kann jede Information über den Benutzer sein, wie z.B. sein Name oder seine E-Mail-Adresse.

Das Zugriffstoken enthält eine Teilmenge der Ansprüche des Benutzers. Dazu zählen alle Anwendungsrollen, denen der Benutzer zugewiesen ist.

Der Parameter principal der Funktion ist ein ClaimsPrincipal-Objekt, das die Ansprüche aus dem Zugriffstoken enthält. Jeder Anspruch ist ein Schlüssel-Wert-Paar aus Anspruchstyp und Anspruchswert. Die Anwendung verwendet es zur Autorisierung der Anforderung.

Die folgende Erweiterungsmethode testet, ob ein ClaimsPrincipal-Objekt einen Satz von Rollen enthält. Sie gibt false zurück, wenn beliebige der angegebenen Rollen fehlen. Wenn diese Methode FALSE zurückgibt, gibt die Funktion den HTTP-Code 401 (Nicht autorisiert) zurück.

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

Weitere Informationen zur Authentifizierung und Autorisierung in dieser Anwendung finden Sie im Abschnitt Sicherheitsüberlegungen der Referenzarchitektur.

Nächste Schritte

Nachdem Sie sich mit der Funktionsweise dieser Referenzlösung vertraut gemacht haben, können Sie sich mit bewährten Methoden und Empfehlungen für ähnliche Lösungen auseinandersetzen.

Azure Functions ist nur eine der Computeoptionen von Azure. Hilfe bei der Auswahl einer Computetechnologie finden Sie unter Entscheidungsstruktur für Azure-Computedienste.