Пошаговое руководство по коду: бессерверное приложение с функциямиCode walkthrough: Serverless application with Functions

Бессерверные модели абстрактный код из базовой инфраструктуры вычислений, позволяющий разработчикам сосредоточиться на бизнес-логике без обширной настройки.Serverless models abstract code from the underlying compute infrastructure, allowing developers to focus on business logic without extensive setup. Несерверный код сокращает затраты, так как вы платите только за ресурсы и длительность выполнения кода.Serverless code reduces costs, because you pay only for the code execution resources and duration.

Модель, управляемая событиями, соответствует ситуациям, когда определенное событие запускает определенное действие.The serverless event-driven model fits situations where a certain event triggers a defined action. Например, получение входящего сообщения устройства, которое активирует хранилище для последующего использования, или обновление базы данных вызывает дальнейшую обработку.For example, receiving an incoming device message triggers storage for later use, or a database update triggers some further processing.

Чтобы помочь вам исследовать бессерверные технологии Azure в Azure, корпорация Майкрософт разработала и протестировала бессерверное приложение, использующее функции Azure.To help you explore Azure serverless technologies in Azure, Microsoft developed and tested a serverless application that uses Azure Functions. В этой статье рассматривается код для решения бессерверных функций, а также рассматриваются решения по проектированию, сведения о реализации и некоторые «проблемы», которые могут возникнуть.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.

Изучение решенияExplore the solution

Решение из двух частей описывает гипотетическую систему доставки помощью дронов.The two-part solution describes a hypothetical drone delivery system. Дроны отправляют данные о состоянии полета в облако, где эти сообщения хранятся для последующего использования.Drones send in-flight status to the cloud, which stores these messages for later use. Веб-приложение позволяет пользователям получать сообщения для получения последнего состояния устройств.A web app lets users retrieve the messages to get the latest status of the devices.

Вы можете скачать код для этого решения с сайта GitHub.You can download the code for this solution from GitHub.

В этом пошаговом руководстве предполагается знание следующих технологий:This walkthrough assumes basic familiarity with the following technologies:

Не обязательно досконально разбираться в функциях или концентраторах событий, но вы должны в общих чертах понимать их возможности.You don't need to be an expert in Functions or Event Hubs, but you should understand their features at a high level. Ниже приведены несколько ресурсов, которые помогут вам начать работу.Here are some good resources to get started:

Ознакомление со сценариемUnderstand the scenario

Схема функциональных единиц

Компания Fabrikam содержит парк дронов для службы доставки с помощью дронов.Fabrikam manages a fleet of drones for a drone delivery service. Приложение состоит из двух основных функциональных областей.The application consists of two main functional areas:

  • Прием событий.Event ingestion. Во время полета дроны сообщают о своем состоянии в облачную конечную точку.During flight, drones send status messages to a cloud endpoint. Приложение принимает и обрабатывает эти сообщения и записывает результаты в серверную базу данных (Cosmos DB).The application ingests and processes these messages, and writes the results to a back-end database (Cosmos DB). Устройства отправляют сообщения в формате буфера протокола (protobuf).The devices send messages in protocol buffer (protobuf) format. Protobuf — это эффективный самоописывающий формат сериализации.Protobuf is an efficient, self-describing serialization format.

    Эти сообщения содержат частичные обновления.These messages contain partial updates. Через фиксированные промежутки времени каждый дрон отправляет сообщение "ключевого кадра", содержащее все поля состояния.At a fixed interval, each drone sends a "key frame" message that contains all of the status fields. Сообщения о состоянии между ключевыми кадрами содержат только поля, которые изменились с момента последнего сообщения.Between key frames, the status messages only include fields that changed since the last message. Такое поведение является типичным для многих устройств Интернета вещей, которым необходимо экономить пропускную способность и мощность.This behavior is typical of many IoT devices that need to conserve bandwidth and power.

  • Веб-приложение.Web app. Веб-приложение позволяет пользователям искать устройства и запрашивать последнее известное состояние устройства.A web application allows users to look up a device and query the device's last-known status. Пользователи должны войти в приложение и пройти проверку подлинности с помощью Azure Active Directory (Azure AD).Users must sign into the application and authenticate with Azure Active Directory (Azure AD). Приложение разрешает отправлять запросы только пользователям, которые имеют право доступа к приложению.The application only allows requests from users who are authorized to access the app.

Ниже приведен снимок экрана веб-приложения с результатом запроса.Here's a screenshot of the web app, showing the result of a query:

Снимок экрана клиентского приложения

Разработка приложенияDesign the application

Компания Fabrikam решила использовать функции Azure для реализации бизнес-логики приложения.Fabrikam has decided to use Azure Functions to implement the application business logic. Функции Azure — это пример "функций как услуги" (FaaS).Azure Functions is an example of "Functions as a Service" (FaaS). В этой вычислительной модели функция — это часть кода, которая развертывается в облаке и выполняется в среде размещения.In this computing model, a function is a piece of code that is deployed to the cloud and runs in a hosting environment. Эта среда размещения полностью отделяет серверы, на которых выполняется код.This hosting environment completely abstracts the servers that run the code.

В чем преимущество бессерверного подхода?Why choose a serverless approach?

Бессерверная архитектура с Функциями Azure является примером управляемой событиями архитектуры.A serverless architecture with Functions is an example of an event-driven architecture. Код функции активируется по некоторому событию, которое этом случае для функции — является внешним — это либо сообщение с дрона, либо HTTP-запрос из клиентского приложения.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. При использовании приложения-функции вам не нужно писать код для триггера.With a function app, you don't need to write any code for the trigger. Вы пишете только код, который выполняется в ответ на триггер.You only write the code that runs in response to the trigger. Это означает, что вы можете сосредоточиться на бизнес-логике вместо написания большого объема кода для обработки особенностей инфраструктуры, например системы обмена сообщениями.That means you can focus on your business logic, rather than writing a lot of code to handle infrastructure concerns like messaging.

Использование бессерверной архитектуры также дает некоторые оперативные преимущества:There are also some operational advantages to using a serverless architecture:

  • нет необходимости управлять серверами;There is no need to manage servers.
  • вычислительные ресурсы динамически распределяются по мере необходимости;Compute resources are allocated dynamically as needed.
  • вы платите только за вычислительные ресурсы, которые используются для выполнения кода.You are charged only for the compute resources used to execute your code.
  • вычислительные ресурсы масштабируются по требованию в зависимости от трафика.The compute resources scale on demand based on traffic.

ArchitectureArchitecture

На схеме ниже в общих чертах показана архитектура приложения.The following diagram shows the high-level architecture of the application:

Architecture

Прием событий:Event ingestion:

  1. сообщения с дронов принимаются Центрами событий Azure;Drone messages are ingested by Azure Event Hubs.
  2. Центры событий формируют поток событий, содержащий данные сообщений;Event Hubs produces a stream of events that contain the message data.
  3. эти события активируют приложение Функций Azure для их обработки;These events trigger an Azure Functions app to process them.
  4. результаты сохраняются в Cosmos DB.The results are stored in Cosmos DB.

Веб-приложение:Web app:

  1. статические файлы обслуживаются сетью доставки содержимого Microsoft Azure из хранилища BLOB-объектов;Static files are served by CDN from Blob storage.
  2. пользователь входит в веб-приложение с помощью Azure AD;A user signs into the web app using Azure AD.
  3. служба Управления API Azure выступает в качестве шлюза, предоставляющего конечную точку REST API;Azure API Management acts as a gateway that exposes a REST API endpoint.
  4. HTTP-запросы от клиента активируют приложение Функций Azure, которое считывает данные из Cosmos DB и возвращает результат.HTTP requests from the client trigger an Azure Functions app that reads from Cosmos DB and returns the result.

Это приложение основано на двух эталонных архитектурах, соответствующих двум функциональным блокам, описанным выше:This application is based on two reference architectures, corresponding to the two functional blocks described above:

Вы можете прочитать эти статьи, чтобы получить дополнительные сведения об архитектуре, службах Azure, которые используются в решении, а также рекомендации по масштабируемости, безопасности и надежности.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.

Функция телеметрии дронаDrone telemetry function

Давайте начнем с рассмотрения функции, которая обрабатывает сообщения с дронов из Центров событий.Let's start by looking at the function that processes drone messages from Event Hubs. Функция определена в классе с именем RawTelemetryFunction.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;
        }
    }
    ...
}

Этот класс имеет несколько зависимостей, которые добавляются в конструктор с помощью внедрения зависимости.This class has several dependencies, which are injected into the constructor using dependency injection:

  • Интерфейсы ITelemetryProcessor и IStateChangeProcessor определяют два вспомогательных объекта.The ITelemetryProcessor and IStateChangeProcessor interfaces define two helper objects. Как мы увидим, эти объекты выполняют большую часть работы.As we'll see, these objects do most of the work.

  • TelemetryClient — это часть пакета SDK Application Insights.The TelemetryClient is part of the Application Insights SDK. Он используется для отправки пользовательских метрик приложения в Application Insights.It is used to send custom application metrics to Application Insights.

Позже мы рассмотрим способы настройки внедрения зависимостей.Later, we'll look at how to configure the dependency injection. Пока просто считайте, что они существуют.For now, just assume these dependencies exist.

Настройка триггера Центров событийConfigure the Event Hubs trigger

Логика в функции реализуется как асинхронный метод с именем RunAsync.The logic in the function is implemented as an asynchronous method named RunAsync. Вот так выглядит подпись метода.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
}

Метод принимает следующие параметры.The method takes the following parameters:

  • messages — массив сообщений концентратора событий.messages is an array of event hub messages.
  • deadLetterMessages — очередь службы хранилища Azure, используемая для хранения недоставленных сообщений.deadLetterMessages is an Azure Storage Queue, used for storing dead letter messages.
  • logging — интерфейс ведения журнала для записи журналов приложения.logging provides a logging interface, for writing application logs. Эти журналы отправляются в Azure Monitor.These logs are sent to Azure Monitor.

Атрибут EventHubTrigger параметра messages настраивает триггер.The EventHubTrigger attribute on the messages parameter configures the trigger. Свойства атрибута определяют имя концентратора событий, строку подключения и группу потребителей.The properties of the attribute specify an event hub name, a connection string, and a consumer group. (Группа потребителей — изолированное представление потоковой передачи событий Центра событий.(A consumer group is an isolated view of the Event Hubs event stream. Эта абстракция позволяет использовать несколько потребителей того же концентратора событий.)This abstraction allows for multiple consumers of the same event hub.)

Обратите внимание на знаки процента (%) в некоторых свойствах атрибута.Notice the percent signs (%) in some of the attribute properties. Они указывают на то, что свойство задает имя параметра приложения, а фактическое значение извлекается из такого параметра приложения во время выполнения.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. В противном случае, без знаков процента, свойство передает буквальное значение.Otherwise, without percent signs, the property gives the literal value.

Свойство Connection — исключение.The Connection property is an exception. Это свойство всегда указывает имя параметра приложения, а не буквальное значение, поэтому знак процента не требуется.This property always specifies an app setting name, never a literal value, so the percent sign is not needed. Это различие связано с тем, что строка подключения является секретной и ее нельзя содержать в исходном коде.The reason for this distinction is that a connection string is secret and should never be checked into source code.

Хотя другие два свойства (имя концентратора событий, а также группа потребителей) не являются конфиденциальными данными, как например строка подключения, их все же лучше поместить в параметры приложения, а не прописывать в коде.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. Таким образом их можно обновлять без перекомпиляции приложения.That way, they can be updated without recompiling the app.

Дополнительные сведения о настройке этого триггера см. в разделе Привязки Центров событий Azure для Функций Azure.For more information about configuring this trigger, see Azure Event Hubs bindings for Azure Functions.

Логика обработки сообщенийMessage processing logic

Вот реализация метода RawTelemetryFunction.RunAsync, который обрабатывает пакет сообщений: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 });
        }
    }
}

Когда функция вызывается, параметр messages содержит массив сообщений из концентратора событий.When the function is invoked, the messages parameter contains an array of messages from the event hub. Пакетная обработка сообщений обычно эффективнее, чем чтение сообщений по одному.Processing messages in batches will generally yield better performance than reading one message at a time. Однако вы должны убедиться, что функция устойчива и корректно обрабатывает сбои и исключения.However, you have to make sure the function is resilient and handles failures and exceptions gracefully. Иначе, если функция выдает необработанное исключение в середине пакета, вы можете потерять оставшиеся сообщения.Otherwise, if the function throws an unhandled exception in the middle of a batch, you might lose the remaining messages. Этот момент более подробно рассматривается в разделе Обработка ошибок.This consideration is discussed in more detail in the section Error handling.

Но если вы игнорируете обработку исключений, логика обработки каждого сообщения проста:But if you ignore the exception handling, the processing logic for each message is simple:

  1. Вызовите ITelemetryProcessor.Deserialize для десериализации сообщения, содержащего изменение состояния устройства.Call ITelemetryProcessor.Deserialize to deserialize the message that contains a device state change.
  2. Вызовите IStateChangeProcessor.UpdateState для обработки изменения состояния.Call IStateChangeProcessor.UpdateState to process the state change.

Давайте подробнее рассмотрим эти два метода, начиная с метода Deserialize.Let's look at these two methods in more detail, starting with the Deserialize method.

Метод десериализацииDeserialize method

Метод TelemetryProcess.Deserialize принимает массив байтов, который содержит полезные данные сообщения.The TelemetryProcess.Deserialize method takes a byte array that contains the message payload. Он десериализует эти полезные данные и возвращает объект DeviceState, который соответствует состоянию беспилотника.It deserializes this payload and returns a DeviceState object, which represents the state of a drone. Состояние может представлять собой частичное обновление, содержащее только отличия от последнего известного состояния.The state may represent a partial update, containing just the delta from the last-known state. Поэтому метод должен обрабатывать поля null в десериализованных полезных данных.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;
    }
}

Этот метод использует другой вспомогательный интерфейс ITelemetrySerializer<T> для десериализации необработанного сообщения.This method uses another helper interface, ITelemetrySerializer<T>, to deserialize the raw message. Затем результаты преобразуются в модель POCO, с которой легче работать.The results are then transformed into a POCO model that is easier to work with. Этот конструктор помогает изолировать логику обработки от деталей реализации сериализации.This design helps to isolate the processing logic from the serialization implementation details. Интерфейс ITelemetrySerializer<T> определен в общей библиотеке, которая также используется симулятором устройства для создания событий имитируемого устройства и отправки их в концентраторы событий.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);
    }
}

Метод UpdateStateUpdateState method

Метод StateChangeProcessor.UpdateState применяет изменения состояния.The StateChangeProcessor.UpdateState method applies the state changes. Последнее известное состояние для каждого беспилотника сохраняется как документ JSON в Cosmos DB.The last-known state for each drone is stored as a JSON document in Cosmos DB. Поскольку беспилотники отправляют частичные обновления, приложение не может просто перезаписать документ во время получения обновления.Because the drones send partial updates, the application can't simply overwrite the document when it gets an update. Вместо этого ему нужно извлечь предыдущее состояние, объединить поля и затем выполнить операцию upsert.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);
    }
}

Этот код использует интерфейс IDocumentClient для получения документа из Cosmos DB.This code uses the IDocumentClient interface to fetch a document from Cosmos DB. Если документ существует, новые значения состояния объединяются в существующий документ.If the document exists, the new state values are merged into the existing document. В противном случае создается новый документ.Otherwise, a new document is created. Оба случая обрабатываются методом UpsertDocumentAsync.Both cases are handled by the UpsertDocumentAsync method.

Этот код оптимизирован для случая, когда документ уже существует и может быть объединен.This code is optimized for the case where the document already exists and can be merged. В первом сообщении телеметрии от данного беспилотника метод ReadDocumentAsync вызовет исключение, поскольку для этого беспилотника нет документа.On the first telemetry message from a given drone, the ReadDocumentAsync method will throw an exception, because there is no document for that drone. После первого сообщения документ уже будет доступен.After the first message, the document will be available.

Обратите внимание, что этот класс использует внедрение зависимостей для внедрения IDocumentClient для Cosmos DB и IOptions<T> с параметрами конфигурации.Notice that this class uses dependency injection to inject the IDocumentClient for Cosmos DB and an IOptions<T> with configuration settings. Настройка внедрения зависимостей показана далее.We'll see how to set up the dependency injection later.

Примечание

Функции Azure поддерживают привязку выходных данных для Cosmos DB.Azure Functions supports an output binding for Cosmos DB. Эта привязка позволяет приложению-функции записывать документы в Cosmos DB без какого-либо кода.This binding lets the function app write documents in Cosmos DB without any code. Однако выходная привязка не будет работать для этого конкретного сценария из-за необходимости наличия пользовательской логики upsert.However, the output binding won't work for this particular scenario, because of the custom upsert logic that's needed.

Обработка ошибокError handling

Как упоминалось ранее, приложение-функция RawTelemetryFunction обрабатывает пакет сообщений в цикле.As mentioned earlier, the RawTelemetryFunction function app processes a batch of messages in a loop. Это означает, что функция должна корректно обрабатывать любые исключения и продолжать обработку оставшейся части пакета.That means the function needs to handle any exceptions gracefully and continue processing the rest of the batch. Иначе сообщения могут быть отброшены.Otherwise, messages might get dropped.

Если при обработке сообщения возникает исключение, функция помещает сообщение в очередь недоставленных сообщений: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 });
}

Очередь недоставленных сообщений определяется с помощью выходной привязки к хранилищу очередей: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)

Здесь атрибут Queue указывает выходную привязку, а атрибут StorageAccount — имя параметра приложения, в котором хранится строка подключения для учетной записи хранилища.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.

Совет по развертыванию: В шаблоне диспетчер ресурсов, который создает учетную запись хранения, можно автоматически заполнять параметр приложения строкой подключения.Deployment tip: In the Resource Manager template that creates the storage account, you can automatically populate an app setting with the connection string. Хитрость заключается в использовании функции listKeys .The trick is to use the listKeys function.

Вот раздел шаблона, который создает учетную запись хранилища для очереди: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')]"
        },

Вот раздел шаблона, который создает функцию приложения.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)]"
                    },
                    ...

Он определяет параметр приложения с именем DeadLetterStorage, значение которого заполняется с помощью функции listKeys.This defines an app setting named DeadLetterStorage whose value is populated using the listKeys function. Важно сделать ресурс приложения-функции зависимым от ресурса учетной записи хранилища (см. элемент dependsOn).It's important to make the function app resource depend on the storage account resource (see the dependsOn element). Это гарантирует, что учетная запись хранилища будет создана первой, а строка подключения будет доступна.This guarantees that the storage account is created first and the connection string is available.

Настройка внедрения зависимостейSetting up dependency injection

Следующий код настраивает внедрение зависимости для функции RawTelemetryFunction: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);
            });
        }
    }
}

Функции Azure, написанные для .NET, могут использовать платформу внедрения зависимостей ASP.NET Core.Azure Functions written for .NET can use the ASP.NET Core dependency injection framework. Основная идея заключается в том, что вы объявляете метод запуска для своей сборки.The basic idea is that you declare a startup method for your assembly. Метод принимает интерфейс IFunctionsHostBuilder, который используется для объявления зависимостей для DI.The method takes an IFunctionsHostBuilder interface, which is used to declare the dependencies for DI. Это можно сделать, вызвав метод Add* объекта Services.You do this by calling Add* method on the Services object. Когда вы добавляете зависимость, то указываете время ее существования:When you add a dependency, you specify its lifetime:

  • Промежуточные объекты создаются каждый раз, когда их запрашивают.Transient objects are created each time they're requested.
  • Объекты с заданной областью создаются один раз во время выполнение функции.Scoped objects are created once per function execution.
  • Отдельные объекты повторно используются при выполнении функций в течение времени существования узла функции.Singleton objects are reused across function executions, within the lifetime of the function host.

В этом примере объекты TelemetryProcessor и StateChangeProcessor объявляются как промежуточные.In this example, the TelemetryProcessor and StateChangeProcessor objects are declared as transient. Это применимо для простых служб без отслеживания состояния.This is appropriate for lightweight, stateless services. С другой стороны, чтобы достичь оптимальной производительности, класс DocumentClient должен быть отдельным.The DocumentClient class, on the other hand, should be a singleton for best performance. Дополнительные сведения см. в статье о советах по повышению производительности в Azure Cosmos DB и .NET.For more information, see Performance tips for Azure Cosmos DB and .NET.

Если вернуться к коду для RawTelemetryFunction, вы увидите другую зависимость, которая не отображается в коде настройки DI, а именно класс TelemetryClient, используемый для метрики журнала приложения.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. Среда выполнения функций автоматически регистрирует этот класс в контейнер DI, так что он не требует явной регистрации.The Functions runtime automatically registers this class into the DI container, so you don't need to register it explicitly.

Дополнительные сведения о DI в Функциях Azure см. в следующих статьях:For more information about DI in Azure Functions, see the following articles:

Передача параметров конфигурации в DIPassing configuration settings in DI

Иногда для объекта требуется инициализация с помощью некоторых значений конфигурации.Sometimes an object must be initialized with some configuration values. Как правило, эти параметры должны исходить из настроек приложения или (при использовании секретов) из хранилища ключей Azure.Generally, these settings should come from app settings or (in the case of secrets) from Azure Key Vault.

В случае этого приложения, есть два примера.There are two examples in this application. Первый — класс DocumentClient берет конечную точку службы Cosmos DB и ключ.First, the DocumentClient class takes a Cosmos DB service endpoint and key. Для этого объекта приложение регистрирует лямбда-выражение, которое будет вызываться с помощью контейнера DI.For this object, the application registers a lambda that will be invoked by the DI container. Это лямбда-выражение использует интерфейс IConfiguration для чтения значений конфигурации: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);
});

Вторым примером является класс StateChangeProcessor.The second example is the StateChangeProcessor class. Для этого объекта мы используем подход, который называется шаблон параметров.For this object, we use an approach called the options pattern. Это работает следующим образом:Here's how it works:

  1. Определите класс T, который содержит параметры конфигурации.Define a class T that contains your configuration settings. В данном случае — имя базы данных Cosmos DB и имя коллекции.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. Добавьте класс T в качестве класса параметров для DI.Add the class T as an options class for DI.

    builder.Services.AddOptions<StateChangeProcessorOptions>()
        .Configure<IConfiguration>((configSection, configuration) =>
        {
            configuration.Bind(configSection);
        });
    
  3. В конструктор класса, который настраивается, включите параметр IOptions<T>.In the constructor of the class that is being configured, include an IOptions<T> parameter.

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

Система DI автоматически заполнит класс параметров, используя значения конфигурации, и передаст его в конструктор.The DI system will automatically populate the options class with configuration values and pass this to the constructor.

У этого подхода есть несколько преимуществ:There are several advantages of this approach:

  • отделение класса от источника значений конфигурации;Decouple the class from the source of the configuration values.
  • легкая настройка разных источников конфигурации, таких как переменные среды или файлы конфигурации JSON;Easily set up different configuration sources, such as environment variables or JSON configuration files.
  • упрошенное модульное тестирование;Simplify unit testing.
  • использование строго типизированных параметров класса, которые имеют меньшую вероятность появления ошибок, чем при передаче скалярных значений.Use a strongly typed options class, which is less error prone than just passing in scalar values.

Функция GetStatusGetStatus function

В другом приложении Функций Azure реализован простой интерфейс REST API для получения последнего известного состояния дрона в этом решении.The other Functions app in this solution implements a simple REST API to get the last-known status of a drone. Эта функция определена в классе с именем GetStatusFunction.This function is defined in a class named GetStatusFunction. Ниже приведен полный код для функции.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);
            }
        }
    }
}

Эта функция использует триггер протокола HTTP для обработки HTTP-запроса GET.This function uses an HTTP trigger to process an HTTP GET request. Функция использует входную привязку Cosmos DB, чтобы получить запрашиваемый документ.The function uses a Cosmos DB input binding to fetch the requested document. Следует учитывать, что эта привязка будет выполнятся до выполнения логики авторизации внутри функции.One consideration is that this binding will run before the authorization logic is performed inside the function. Если неавторизованный пользователь запрашивает документ, привязка функции по-прежнему будет получать его.If an unauthorized user requests a document, the function binding will still fetch the document. Затем код авторизации возвратит 401, поэтому пользователь не увидит документ.Then the authorization code will return a 401, so the user won't see the document. Соответствие данной реакции на событие может зависеть от требований.Whether this behavior is acceptable may depend on your requirements. Например, этот подход может усложнить доступ к конфиденциальным данным при аудите данных.For example, this approach might make it harder to audit data access for sensitive data.

Аутентификация и авторизацияAuthentication and authorization

Веб-приложение использует Azure AD для выполнения проверки подлинности пользователей.The web app uses Azure AD to authenticate users. Так как приложение является одностраничным приложением, которое выполняется в браузере, поток неявного предоставления разрешения подходит для:Because the app is a single-page application (SPA) running in the browser, the implicit grant flow is appropriate:

  1. перенаправления веб-приложением пользователя к поставщику удостоверений (в данном случае Azure AD);The web app redirects the user to the identity provider (in this case, Azure AD).
  2. ввода пользователем своих учетных данных;The user enters their credentials.
  3. перенаправления поставщиком удостоверений веб-приложения с помощью маркера доступа;The identity provider redirects back to the web app with an access token.
  4. отправки запроса веб-приложением в веб-API и включения маркера доступа в заголовок Авторизации.The web app sends a request to the web API and includes the access token in the Authorization header.

Схема процесса при использовании неявного типа

Приложение Функции можно настроить для проверки подлинности пользователей без кода.A Function application can be configured to authenticate users with zero code. Дополнительные сведения см. в статье Проверка подлинности и авторизация в службе приложений Azure.For more information, see Authentication and authorization in Azure App Service.

С другой стороны, для авторизации обычно требуется бизнес-логика.Authorization, on the other hand, generally requires some business logic. Azure AD поддерживает проверку подлинности на основе утверждений.Azure AD supports claims based authentication. В этой модели удостоверение пользователя представлено как набор утверждений, которые поступают от поставщика удостоверений.In this model, a user's identity is represented as a set of claims that come from the identity provider. Утверждением может быть любой фрагмент сведений о пользователе, например его имя или адрес электронной почты.A claim can be any piece of information about the user, such as their name or email address.

Маркер доступа содержит подмножество утверждений пользователей.The access token contains a subset of user claims. Среди них — любые роли приложения, которым назначен пользователь.Among these are any application roles that the user is assigned to.

Параметр principal функции является объектом ClaimsPrincipal, который содержит утверждения из маркера доступа.The principal parameter of the function is a ClaimsPrincipal object that contains the claims from the access token. Каждое утверждение являет собой пару ключ/значение типа и значения утверждения.Each claim is a key/value pair of claim type and claim value. Они используются приложением для авторизации запроса.The application uses these to authorize the request.

Следующий метод расширения проверяет, содержит ли объект ClaimsPrincipal набор ролей.The following extension method tests whether a ClaimsPrincipal object contains a set of roles. Он возвращает false, если любая из указанных ролей отсутствует.It returns false if any of the specified roles is missing. Если этот метод возвращает значение false, функция возвращает код HTTP 401 (недостаточно прав).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;
        }
    }
}

Дополнительные сведения о проверке подлинности и авторизации в этом приложении см. в разделе Вопросы безопасности эталонной архитектуры.For more information about authentication and authorization in this application, see the Security considerations section of the reference architecture.

Дальнейшие действияNext steps

Получив представление о том, как работает это эталонное решение, ознакомьтесь с рекомендациями и рекомендациями по аналогичным решениям.Once you get a feel for how this reference solution works, learn best practices and recommendations for similar solutions.

Функции Azure — это всего лишь один вариант вычислений Azure.Azure Functions is just one Azure compute option. Сведения о выборе технологии вычислений см. в статье Выбор службы вычислений Azure для приложения.For help with choosing a compute technology, see Choose an Azure compute service for your application.