Exemplarische Vorgehensweise mit Code: Serverlose Anwendung mit Azure FunctionsCode walkthrough: Serverless application with Functions

Serverlose Modelle abstrahieren Code aus der zugrunde liegenden Compute-Infrastruktur, sodass sich Entwickler ohne umfassendes Setup auf die Geschäftslogik konzentrieren können.Serverless models abstract code from the underlying compute infrastructure, allowing developers to focus on business logic without extensive setup. Serverloser Code senkt Kosten, da Sie nur für die Ressourcen und die Dauer der Codeausführung bezahlen müssen.Serverless code reduces costs, because you pay only for the code execution resources and duration.

Das serverlose ereignisgesteuerte Modell ist für Situationen geeignet, in denen ein bestimmtes Ereignis eine definierte Aktion auslöst.The serverless event-driven model fits situations where a certain event triggers a defined action. 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.For example, receiving an incoming device message triggers storage for later use, or a database update triggers some further processing.

Um Sie bei der Erkundung serverloser Azure-Technologien zu unterstützen, hat Microsoft eine serverlose Anwendung entwickelt und getestet, die Azure Functions verwendet.To help you explore Azure serverless technologies in Azure, Microsoft developed and tested a serverless application that uses Azure Functions. 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.This article walks through the code for the serverless Functions solution, and describes design decisions, implementation details, and some of the "gotchas" you might encounter.

Erkunden der LösungExplore the solution

Die zweiteilige Lösung beschreibt ein hypothetisches Auslieferungssystem mit Drohnen.The two-part solution describes a hypothetical drone delivery system. Drohnen senden den Flugstatus an die Cloud, wo diese Nachrichten zur späteren Verwendung gespeichert werden.Drones send in-flight status to the cloud, which stores these messages for later use. Mit einer Web-App können Benutzern die Nachrichten abrufen und den aktuellen Status der Geräte erfahren.A web app lets users retrieve the messages to get the latest status of the devices.

Sie können den Code für diese Lösung von GitHub herunterladen.You can download the code for this solution from GitHub.

Diese exemplarische Vorgehensweise setzt ein grundlegendes Maß an Vertrautheit mit den folgenden Technologien voraus:This walkthrough assumes basic familiarity with the following technologies:

Sie müssen kein Experte für Azure Functions oder Event Hubs sein, jedoch sollten Sie mit den jeweiligen Features allgemein vertraut sein.You don't need to be an expert in Functions or Event Hubs, but you should understand their features at a high level. Hier einige geeignete Ressourcen für Ihren Einstieg:Here are some good resources to get started:

Das SzenarioUnderstand the scenario

Diagramm der Funktionsblöcke

Fabrikam betreibt eine Flotte von Drohnen für einen Drohnenlieferdienst.Fabrikam manages a fleet of drones for a drone delivery service. Die Anwendung besteht aus zwei Hauptfunktionsbereichen:The application consists of two main functional areas:

  • Ereigniserfassung.Event ingestion. Während des Flugs senden Drohnen Statusmeldungen an einen Cloudendpunkt.During flight, drones send status messages to a cloud endpoint. Die Anwendung erfasst und verarbeitet diese Nachrichten und schreibt die Ergebnisse in eine Back-End-Datenbank (Cosmos DB).The application ingests and processes these messages, and writes the results to a back-end database (Cosmos DB). Die Geräte senden Nachrichten im Protocol Buffers-Format (protobuf).The devices send messages in protocol buffer (protobuf) format. protobuf ist ein effizientes und selbstbeschreibendes Serialisierungsformat.Protobuf is an efficient, self-describing serialization format.

    Diese Nachrichten enthalten Teilupdates.These messages contain partial updates. In einem festen Intervall sendet jede Drohne eine „Keyframe“-Nachricht, die alle Statusfelder enthält.At a fixed interval, each drone sends a "key frame" message that contains all of the status fields. Zwischen Keyframes enthalten die Statusmeldungen nur Felder, die sich seit der letzten Nachricht geändert haben.Between key frames, the status messages only include fields that changed since the last message. Dieses Verhalten ist typisch für viele IoT-Geräte, die mit Bandbreite und Energie sparsam umgehen müssen.This behavior is typical of many IoT devices that need to conserve bandwidth and power.

  • Web-App.Web app. Eine Webanwendung ermöglicht Benutzern, ein Gerät nachzuschlagen und den letzten bekannten Status des Geräts abzufragen.A web application allows users to look up a device and query the device's last-known status. Benutzer müssen sich bei der Anwendung anmelden und mithilfe von Azure Active Directory (Azure AD) authentifizieren.Users must sign into the application and authenticate with Azure Active Directory (Azure AD). Die Anwendung lässt nur Anforderungen von Benutzern zu, die zum Zugriff auf die App berechtigt sind.The application only allows requests from users who are authorized to access the app.

Hier sehen Sie einen Screenshot der Webanwendung, der das Ergebnis einer Abfrage zeigt:Here's a screenshot of the web app, showing the result of a query:

Screenshot der Client-App

Entwerfen der AnwendungDesign the application

Fabrikam hat sich entschieden, zur Implementierung der Geschäftslogik der Anwendung Azure Functions einzusetzen.Fabrikam has decided to use Azure Functions to implement the application business logic. Azure Functions ist ein Beispiel für „Functions as a Service“ (FaaS).Azure Functions is an example of "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 this computing model, a function is a piece of code that is deployed to the cloud and runs in a hosting environment. In dieser Hostingumgebung werden die Server, auf denen der Code ausgeführt wird, vollständig abstrahiert.This hosting environment completely abstracts the servers that run the code.

Was spricht für einen serverlosen Ansatz?Why choose a serverless approach?

Eine serverlose Architektur mit Azure Functions ist ein Beispiel einer ereignisgesteuerten Architektur.A serverless architecture with Functions is an example of an event-driven architecture. 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.The function code is a triggered by some event that's external to the function — in this case, either a message from a drone, or an HTTP request from a client application. Bei einer Funktions-App müssen Sie keinen Code für den Trigger schreiben.With a function app, you don't need to write any code for the trigger. Sie schreiben lediglich den Code, der als Reaktion auf den Trigger ausgeführt wird.You only write the code that runs in response to the trigger. 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.That means you can focus on your business logic, rather than writing a lot of code to handle infrastructure concerns like messaging.

Der Einsatz einer serverlosen Architektur bringt auch betriebliche Vorteile:There are also some operational advantages to using a serverless architecture:

  • Es müssen keine Server verwaltet werden.There is no need to manage servers.
  • Computeressourcen werden je nach Anforderungen dynamisch zugeteilt.Compute resources are allocated dynamically as needed.
  • Ihnen werden nur die Computeressourcen berechnet, die zum Ausführen des Codes verwendet werden.You are charged only for the compute resources used to execute your code.
  • Die Computeressourcen lassen sich basierend auf dem Datenverkehr je nach Bedarf skalieren.The compute resources scale on demand based on traffic.

AufbauArchitecture

Das folgende Diagramm zeigt die allgemeine Architektur der Anwendung:The following diagram shows the high-level architecture of the application:

Aufbau

Ereigniserfassung:Event ingestion:

  1. Nachrichten von Drohnen werden von Azure Event Hubs erfasst.Drone messages are ingested by Azure Event Hubs.
  2. Event Hubs erzeugt einen Ereignisdatenstrom, der die Nachrichtendaten enthält.Event Hubs produces a stream of events that contain the message data.
  3. Diese Ereignisse lösen eine Azure Functions-App aus, mit der sie verarbeitet werden.These events trigger an Azure Functions app to process them.
  4. Die Ergebnisse werden in Cosmos DB gespeichert.The results are stored in Cosmos DB.

Web-App:Web app:

  1. Statische Dateien werden vom CDN aus Blob Storage bereitgestellt.Static files are served by CDN from Blob storage.
  2. Ein Benutzer meldet sich mithilfe von Azure AD bei der Web-App an.A user signs into the web app using Azure AD.
  3. Azure API Management fungiert als Gateway, das einen REST-API-Endpunkt verfügbar macht.Azure API Management acts as a gateway that exposes a REST API endpoint.
  4. HTTP-Anforderungen vom Client lösen eine Azure Functions-App aus, die Daten aus Cosmos DB liest und das Ergebnis zurückgibt.HTTP requests from the client trigger an Azure Functions app that reads from Cosmos DB and returns the result.

Diese Anwendung basiert auf zwei Referenzarchitekturen, die den beiden zuvor beschriebenen Funktionsblöcken entsprechen:This application is based on two reference architectures, corresponding to the two functional blocks described above:

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.You can read those articles to learn more about the high-level architecture, the Azure services that are used in the solution, and considerations for scalability, security, and reliability.

Telemetriefunktion von DrohnenDrone telemetry function

Beginnen wir mit der Funktion, die von Event Hubs stammende Drohnennachrichten verarbeitet.Let's start by looking at the function that processes drone messages from Event Hubs. Die Funktion ist in einer Klasse mit dem Namen RawTelemetryFunction definiert:The function is defined in a class named 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;
        }
    }
    ...
}

Diese Klasse hat mehrere Abhängigkeiten, die per Abhängigkeitsinjektion in den Konstruktor injiziert werden:This class has several dependencies, which are injected into the constructor using dependency injection:

  • Die Schnittstellen ITelemetryProcessor und IStateChangeProcessor definieren zwei Hilfsobjekte.The ITelemetryProcessor and IStateChangeProcessor interfaces define two helper objects. Wie wir sehen werden, verrichten diese Objekte die meiste Arbeit.As we'll see, these objects do most of the work.

  • Das TelemetryClient-Element ist Teil des Application Insights SDK.The TelemetryClient is part of the Application Insights SDK. Es wird zum Senden benutzerdefinierter Metriken an Application Insights verwendet.It is used to send custom application metrics to Application Insights.

Später sehen wir uns an, wie die Abhängigkeitsinjektion konfiguriert wird.Later, we'll look at how to configure the dependency injection. Im Moment gehen wir einfach davon aus, dass diese Abhängigkeiten existieren.For now, just assume these dependencies exist.

Konfigurieren des Event Hubs-TriggersConfigure the Event Hubs trigger

Die Logik in der Funktion ist als asynchrone Methode mit dem Namen RunAsync implementiert.The logic in the function is implemented as an asynchronous method named RunAsync. Hier die Signatur der Methode:Here is the method signature:

[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:The method takes the following parameters:

  • messages ist ein Array von Event Hub-Nachrichten.messages is an array of event hub messages.
  • deadLetterMessages ist eine Azure Storage-Warteschlange zum Speichern unzustellbarer Nachrichten.deadLetterMessages is an Azure Storage Queue, used for storing dead letter messages.
  • logging bietet eine Protokollierungsschnittstelle für das Schreiben von Anwendungsprotokollen.logging provides a logging interface, for writing application logs. Diese Protokolle werden an Azure Monitor gesendet.These logs are sent to Azure Monitor.

Mit dem Attribut EventHubTrigger für den Parameter messages wird der Trigger konfiguriert.The EventHubTrigger attribute on the messages parameter configures the trigger. Die Eigenschaften des Attributs geben einen Event Hub-Namen, eine Verbindungszeichenfolge und eine Consumergruppe an.The properties of the attribute specify an event hub name, a connection string, and a consumer group. (Eine Consumergruppe ist eine isolierte Ansicht des Event Hubs-Ereignisdatenstroms.(A consumer group is an isolated view of the Event Hubs event stream. Diese Abstraktion lässt mehrere Consumer desselben Event Hubs zu.)This abstraction allows for multiple consumers of the same event hub.)

Beachten Sie die Prozentzeichen (%) in einigen der Eigenschaften des Attributs.Notice the percent signs (%) in some of the attribute properties. Sie bedeuten, dass die Eigenschaft den Namen einer App-Einstellung angibt und der tatsächliche Wert zur Laufzeit dieser App-Einstellung entnommen wird.These indicate that the property specifies the name of an app setting, and the actual value is taken from that app setting at run time. Ohne Prozentzeichen gibt die Eigenschaft den Literalwert an.Otherwise, without percent signs, the property gives the literal value.

Die Connection-Eigenschaft ist eine Ausnahme.The Connection property is an exception. Diese Eigenschaft gibt stets den Namen der App-Einstellung und keinen Literalwert an, weshalb das Prozentzeichen nicht benötigt wird.This property always specifies an app setting name, never a literal value, so the percent sign is not needed. Der Grund für diese Unterscheidung ist, dass eine Verbindungszeichenfolge geheim ist und niemals in den Quellcode eingecheckt werden sollte.The reason for this distinction is that a connection string is secret and should never be checked into source code.

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.While the other two properties (event hub name and consumer group) are not sensitive data like a connection string, it's still better to put them into app settings, rather than hard coding. Auf diese Weise können sie aktualisiert werden, ohne die App neu zu kompilieren.That way, they can be updated without recompiling the app.

Weitere Informationen zum Konfigurieren dieses Triggers finden Sie unter Azure Event Hubs-Bindungen für Azure Functions.For more information about configuring this trigger, see Azure Event Hubs bindings for Azure Functions.

NachrichtenverarbeitungslogikMessage processing logic

Hier ist die Implementierung der RawTelemetryFunction.RunAsync-Methode, die einen Batch von Nachrichten verarbeitet:Here's the implementation of the RawTelemetryFunction.RunAsync method that processes a batch of messages:

[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.When the function is invoked, the messages parameter contains an array of messages from the event hub. Die Batchverarbeitung von Nachrichten führt in der Regel zu einer besseren Leistung als das sequenzielle Lesen von Nachrichten.Processing messages in batches will generally yield better performance than reading one message at a time. Sie müssen jedoch sicherstellen, dass die Funktion zuverlässig ist und Fehler und Ausnahmen ordnungsgemäß behandelt werden.However, you have to make sure the function is resilient and handles failures and exceptions gracefully. Andernfalls können, wenn die Funktion mitten in einem Batch eine unbehandelte Ausnahme auslöst, die restlichen Nachrichten verloren gehen.Otherwise, if the function throws an unhandled exception in the middle of a batch, you might lose the remaining messages. Dieser Aspekt wird im Abschnitt Fehlerbehandlung ausführlicher erörtert.This consideration is discussed in more detail in the section Error handling.

Wenn Sie jedoch die Ausnahmebehandlung ignorieren, ist die Verarbeitungslogik für jede Nachricht einfach:But if you ignore the exception handling, the processing logic for each message is simple:

  1. Rufen Sie ITelemetryProcessor.Deserialize auf, um die Nachricht zu deserialisieren, die eine Änderung des Gerätezustands enthält.Call ITelemetryProcessor.Deserialize to deserialize the message that contains a device state change.
  2. Rufen Sie IStateChangeProcessor.UpdateState auf, um die Zustandsänderung zu verarbeiten.Call IStateChangeProcessor.UpdateState to process the state change.

Lassen Sie uns diese beiden Methoden näher betrachten, wobei wir mit der Deserialize-Methode beginnen.Let's look at these two methods in more detail, starting with the Deserialize method.

DeserialisierungsmethodeDeserialize method

Die TelemetryProcess.Deserialize-Methode verwendet ein Bytearray, das die Nachrichtennutzlast enthält.The TelemetryProcess.Deserialize method takes a byte array that contains the message payload. Sie deserialisiert diese Nutzlast und gibt ein DeviceState-Objekt zurück, das den Zustand einer Drohne darstellt.It deserializes this payload and returns a DeviceState object, which represents the state of a drone. Der Zustand kann ein Teilupdate darstellen, das nur das Delta des letzten bekannten Zustandes enthält.The state may represent a partial update, containing just the delta from the last-known state. Daher muss die Methode Felder des Typs null in der deserialisierten Nutzlast behandeln.Therefore, the method needs to handle null fields in the deserialized payload.

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.This method uses another helper interface, ITelemetrySerializer<T>, to deserialize the raw message. Die Ergebnisse werden dann in ein POCO-Modell umgewandelt, mit dem sich leichter arbeiten lässt.The results are then transformed into a POCO model that is easier to work with. Dieser Ansatz hilft, die Verarbeitungslogik von den Details der Implementierung der Serialisierung zu trennen.This design helps to isolate the processing logic from the serialization implementation details. 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.The ITelemetrySerializer<T> interface is defined in a shared library, which is also used by the device simulator to generate simulated device events and send them to Event Hubs.

using System;

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

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

UpdateState-MethodeUpdateState method

Die StateChangeProcessor.UpdateState-Methode wendet die Zustandsänderungen an.The StateChangeProcessor.UpdateState method applies the state changes. Der letzte bekannte Zustand jeder Drohne wird als JSON-Dokument in Cosmos DB gespeichert.The last-known state for each drone is stored as a JSON document in Cosmos DB. Da die Drohnen Teilupdates senden, kann die Anwendung das Dokument nicht einfach überschreiben, sobald sie ein Update erhält.Because the drones send partial updates, the application can't simply overwrite the document when it gets an update. Stattdessen muss es den vorherigen Zustand abrufen, die Felder zusammenführen und dann einen Upsert-Vorgang durchführen.Instead, it needs to fetch the previous state, merge the fields, and then perform an upsert operation.

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.This code uses the IDocumentClient interface to fetch a document from Cosmos DB. Wenn das Dokument vorhanden ist, werden die neuen Zustandswerte mit dem bestehenden Dokument zusammengeführt.If the document exists, the new state values are merged into the existing document. Andernfalls wird ein neues Dokument erstellt.Otherwise, a new document is created. Beide Fälle werden von der UpsertDocumentAsync-Methode verarbeitet.Both cases are handled by the UpsertDocumentAsync method.

Dieser Code ist für den Fall optimiert, dass das Dokument bereits vorhanden ist und zusammengeführt werden kann.This code is optimized for the case where the document already exists and can be merged. Bei der ersten Telemetrienachricht einer bestimmten Drohne löst die ReadDocumentAsync-Methode eine Ausnahme aus, da es für diese Drohne kein Dokument gibt.On the first telemetry message from a given drone, the ReadDocumentAsync method will throw an exception, because there is no document for that drone. Nach der ersten Nachricht ist das Dokument verfügbar.After the first message, the document will be available.

Beachten Sie, dass diese Klasse die Abhängigkeitsinjektion verwendet, um den IDocumentClient für Cosmos DB und IOptions<T> mit Konfigurationseinstellungen zu injizieren.Notice that this class uses dependency injection to inject the IDocumentClient for Cosmos DB and an IOptions<T> with configuration settings. Wir zeigen später, wie die Abhängigkeitsinjektion eingerichtet wird.We'll see how to set up the dependency injection later.

Hinweis

Azure Functions bietet eine Ausgabebindung für Cosmos DB.Azure Functions supports an output binding for Cosmos DB. Diese Bindung ermöglicht der Funktions-App, ohne Code Dokumente in Cosmos DB zu schreiben.This binding lets the function app write documents in Cosmos DB without any code. Allerdings funktioniert die Ausgabebindung für dieses spezielle Szenario aufgrund der benötigten benutzerdefinierten Upsert-Logik nicht.However, the output binding won't work for this particular scenario, because of the custom upsert logic that's needed.

FehlerbehandlungError handling

Wie bereits erwähnt, verarbeitet die Funktions-App RawTelemetryFunction einen Batch von Nachrichten in einer Schleife.As mentioned earlier, the RawTelemetryFunction function app processes a batch of messages in a loop. Dies bedeutet, dass die Funktion alle Ausnahmen ordnungsgemäß behandeln und den Rest des Batches weiterverarbeiten muss.That means the function needs to handle any exceptions gracefully and continue processing the rest of the batch. Andernfalls können Nachrichten verloren gehen.Otherwise, messages might get dropped.

Wenn beim Verarbeiten einer Nachricht eine Ausnahme eintritt, legt die Funktion die Nachricht in einer Warteschlange für unzustellbare Nachrichten ab:If an exception is encountered when processing a message, the function puts the message onto a dead-letter queue:

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:The dead-letter queue is defined using an output binding to a storage queue:

[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.Here the Queue attribute specifies the output binding, and the StorageAccount attribute specifies the name of an app setting that holds the connection string for the storage account.

Bereitstellungstipp: In der Resource Manager-Vorlage für das Erstellen des Speicherkontos können Sie eine App-Einstellung automatisch mit der Verbindungszeichenfolge auffüllen.Deployment tip: In the Resource Manager template that creates the storage account, you can automatically populate an app setting with the connection string. Der Trick dabei ist die Verwendung der listKeys-Funktion.The trick is to use the listKeys function.

Hier ist der Abschnitt der Vorlage, in dem das Speicherkonto für die Warteschlange angelegt wird:Here is the section of the template that creates the storage account for the queue:

    {
        "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.Here is the section of the template that creates the function app.


    {
        "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.This defines an app setting named DeadLetterStorage whose value is populated using the listKeys function. Es ist wichtig, die Funktions-App-Ressource von der Speicherkontenressource abhängig zu machen (siehe das dependsOn-Element).It's important to make the function app resource depend on the storage account resource (see the dependsOn element). Dadurch wird sichergestellt, dass das Speicherkonto zuerst erstellt wird und die Verbindungszeichenfolge zur Verfügung steht.This guarantees that the storage account is created first and the connection string is available.

Einrichten der AbhängigkeitsinjektionSetting up dependency injection

Mit dem folgenden Code wird die Abhängigkeitsinjektion für die Funktion RawTelemetryFunction eingerichtet:The following code sets up dependency injection for the RawTelemetryFunction function:

[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.Azure Functions written for .NET can use the ASP.NET Core dependency injection framework. Die Grundidee ist, dass Sie für Ihre Assembly eine Startmethode deklarieren.The basic idea is that you declare a startup method for your assembly. Die Methode verwendet eine IFunctionsHostBuilder-Schnittstelle, die zum Deklarieren von Abhängigkeiten für die Abhängigkeitsinjektion verwendet wird.The method takes an IFunctionsHostBuilder interface, which is used to declare the dependencies for DI. Rufen Sie dazu für das Services-Objekt die Add*-Methode auf.You do this by calling Add* method on the Services object. Wenn Sie eine Abhängigkeit hinzufügen, geben Sie ihre Lebensdauer an:When you add a dependency, you specify its lifetime:

  • Kurzlebige Objekte werden jedes Mal erstellt, wenn sie angefordert werden.Transient objects are created each time they're requested.
  • Bereichsbezogene Objekte werden einmal pro Funktionsausführung erstellt.Scoped objects are created once per function execution.
  • Singleton-Objekte werden für Ausführungen der Funktion innerhalb der Lebensdauer des Funktionshosts wiederverwendet.Singleton objects are reused across function executions, within the lifetime of the function host.

In diesem Beispiel die Objekte TelemetryProcessor und StateChangeProcessor als kurzlebig deklariert.In this example, the TelemetryProcessor and StateChangeProcessor objects are declared as transient. Dies ist für einfache, zustandslose Dienste sinnvoll.This is appropriate for lightweight, stateless services. Die DocumentClient-Klasse hingegen sollte zum Erzielen der besten Leistung ein Singleton sein.The DocumentClient class, on the other hand, should be a singleton for best performance. Weitere Informationen finden Sie im Artikel zu den Leistungstipps für Azure Cosmos DB und .NET.For more information, see Performance tips for Azure Cosmos DB and .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.If you refer back to the code for the RawTelemetryFunction, you'll see there another dependency that doesn't appear in DI setup code, namely the TelemetryClient class that is used to log application metrics. Die Azure Functions-Laufzeit registriert diese Klasse automatisch im Container für die Abhängigkeitsinjektion, damit Sie sie nicht explizit registrieren müssen.The Functions runtime automatically registers this class into the DI container, so you don't need to register it explicitly.

Weitere Informationen zur Abhängigkeitsinjektion in Azure Functions finden Sie in den folgenden Artikeln:For more information about DI in Azure Functions, see the following articles:

Übergeben von Konfigurationseinstellungen bei der AbhängigkeitsinjektionPassing configuration settings in DI

Mitunter muss ein Objekt mit einigen Konfigurationswerten initialisiert werden.Sometimes an object must be initialized with some configuration values. Im Allgemeinen sollten diese Einstellungen aus den App-Einstellungen oder (im Fall von Geheimnissen) aus Azure Key Vault stammen.Generally, these settings should come from app settings or (in the case of secrets) from Azure Key Vault.

Es gibt in dieser Anwendung zwei Beispiele.There are two examples in this application. Zuerst verwendet die DocumentClient-Klasse einen Cosmos DB-Dienstendpunkt und -Schlüssel.First, the DocumentClient class takes a Cosmos DB service endpoint and key. Für dieses Objekt registriert die Anwendung einen Lambda-Ausdruck, der vom Container für die Abhängigkeitsinjektion aufgerufen wird.For this object, the application registers a lambda that will be invoked by the DI container. Dieser Lambda-Ausdruck verwendet die IConfiguration-Schnittstelle, um die Konfigurationswerte zu lesen:This lambda uses the IConfiguration interface to read the configuration values:

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.The second example is the StateChangeProcessor class. Für dieses Objekt verwenden wir einen Ansatz, der Optionsmuster genannt wird.For this object, we use an approach called the options pattern. Funktionsweise:Here's how it works:

  1. Definieren Sie eine T-Klasse, die Ihre Konfigurationseinstellungen enthält.Define a class T that contains your configuration settings. Dies sind in diesem Fall der Name der Cosmos DB-Datenbank und der Name der Sammlung.In this case, the Cosmos DB database name and collection name.

    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.Add the class T as an options class for DI.

    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.In the constructor of the class that is being configured, include an IOptions<T> parameter.

    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.The DI system will automatically populate the options class with configuration values and pass this to the constructor.

Dieser Ansatz hat mehrere Vorteile:There are several advantages of this approach:

  • Entkopplung der Klasse von der Quelle der Konfigurationswerte.Decouple the class from the source of the configuration values.
  • Einfaches Einrichten verschiedener Konfigurationsquellen, z.B. Umgebungsvariablen oder JSON-Konfigurationsdateien.Easily set up different configuration sources, such as environment variables or JSON configuration files.
  • Vereinfachte Komponententests.Simplify unit testing.
  • Verwenden einer stark typisierten Optionsklasse, was weniger fehleranfällig als das Übergeben von Skalarwerten ist.Use a strongly typed options class, which is less error prone than just passing in scalar values.

GetStatus-FunktionGetStatus function

Die andere Azure Functions-App in dieser Lösung implementiert eine einfache REST-API, um den letzten bekannten Status einer Drohne abzurufen.The other Functions app in this solution implements a simple REST API to get the last-known status of a drone. Diese Funktion ist in einer Klasse mit dem Namen GetStatusFunction definiert.This function is defined in a class named GetStatusFunction. Hier der vollständige Code der Funktion:Here is the complete code for the function:

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.This function uses an HTTP trigger to process an HTTP GET request. Die Funktion verwendet eine Cosmos DB-Eingabebindung, um das angeforderte Dokument abzurufen.The function uses a Cosmos DB input binding to fetch the requested document. Ein Aspekt ist, dass diese Bindung ausgeführt wird, bevor die Berechtigungslogik innerhalb der Funktion ausgeführt wird.One consideration is that this binding will run before the authorization logic is performed inside the function. Wenn ein nicht autorisierter Benutzer ein Dokument anfordert, ruft die Funktionsbindung das Dokument trotzdem ab.If an unauthorized user requests a document, the function binding will still fetch the document. Anschließend gibt der Autorisierungscode den Code 401 zurück, sodass der Benutzer das Dokument nicht sehen kann.Then the authorization code will return a 401, so the user won't see the document. Ob dieses Verhalten akzeptabel ist, hängt von Ihren Anforderungen ab.Whether this behavior is acceptable may depend on your requirements. Dieser Ansatz kann es beispielsweise erschweren, den Datenzugriff auf sensible Daten zu überprüfen.For example, this approach might make it harder to audit data access for sensitive data.

Authentifizierung und AutorisierungAuthentication and authorization

Die Web-App verwendet Azure AD, um Benutzer zu authentifizieren.The web app uses Azure AD to authenticate users. Da die App eine Single-Page-Webanwendung ist, die im Browser ausgeführt wird, ist der Flow der impliziten Gewährung sinnvoll:Because the app is a single-page application (SPA) running in the browser, the implicit grant flow is appropriate:

  1. Die Web-App leitet den Benutzer zum Identitätsanbieter (in diesem Fall Azure AD) um.The web app redirects the user to the identity provider (in this case, Azure AD).
  2. Der Benutzer gibt seine Anmeldeinformation ein.The user enters their credentials.
  3. Der Identitätsanbieter nimmt eine Umleitung mit einem Zugriffstoken zurück zur Web-App vor.The identity provider redirects back to the web app with an access token.
  4. Die Web-App sendet eine Anforderung an die Web-API und fügt das Zugriffstoken in den Autorisierungsheader ein.The web app sends a request to the web API and includes the access token in the Authorization header.

Diagramm des impliziten Flows

Eine Azure Functions-Anwendung kann so konfiguriert werden, dass Benutzer ohne Code authentifiziert werden.A Function application can be configured to authenticate users with zero code. Weitere Informationen finden Sie unter Authentifizierung und Autorisierung in Azure App Service.For more information, see Authentication and authorization in Azure App Service.

Die Autorisierung hingegen erfordert in der Regel eine gewisse Geschäftslogik.Authorization, on the other hand, generally requires some business logic. Azure AD unterstützt die anspruchsbasierte Authentifizierung.Azure AD supports claims based authentication. In diesem Modell wird die Identität eines Benutzers als eine Reihe von Ansprüchen dargestellt, die vom Identitätsanbieter stammen.In this model, a user's identity is represented as a set of claims that come from the identity provider. Ein Anspruch kann jede Information über den Benutzer sein, wie z.B. sein Name oder seine E-Mail-Adresse.A claim can be any piece of information about the user, such as their name or email address.

Das Zugriffstoken enthält eine Teilmenge der Ansprüche des Benutzers.The access token contains a subset of user claims. Dazu zählen alle Anwendungsrollen, denen der Benutzer zugewiesen ist.Among these are any application roles that the user is assigned to.

Der Parameter principal der Funktion ist ein ClaimsPrincipal-Objekt, das die Ansprüche aus dem Zugriffstoken enthält.The principal parameter of the function is a ClaimsPrincipal object that contains the claims from the access token. Jeder Anspruch ist ein Schlüssel-Wert-Paar aus Anspruchstyp und Anspruchswert.Each claim is a key/value pair of claim type and claim value. Die Anwendung verwendet es zur Autorisierung der Anforderung.The application uses these to authorize the request.

Die folgende Erweiterungsmethode testet, ob ein ClaimsPrincipal-Objekt einen Satz von Rollen enthält.The following extension method tests whether a ClaimsPrincipal object contains a set of roles. Sie gibt false zurück, wenn beliebige der angegebenen Rollen fehlen.It returns false if any of the specified roles is missing. Wenn diese Methode FALSE zurückgibt, gibt die Funktion den HTTP-Code 401 (Nicht autorisiert) zurück.If this method returns false, the function returns HTTP 401 (Unauthorized).

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.For more information about authentication and authorization in this application, see the Security considerations section of the reference architecture.

Nächste SchritteNext steps

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.Once you get a feel for how this reference solution works, learn best practices and recommendations for similar solutions.

Azure Functions ist nur eine der Computeoptionen von Azure.Azure Functions is just one Azure compute option. Hilfe bei der Auswahl einer Computetechnologie finden Sie unter Entscheidungsstruktur für Azure-Computedienste.For help with choosing a compute technology, see Choose an Azure compute service for your application.