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

Бессерверные модели абстрактный код из базовой инфраструктуры вычислений, позволяющий разработчикам сосредоточиться на бизнес-логике без обширной настройки. Несерверный код сокращает затраты, так как вы платите только за ресурсы и длительность выполнения кода.

Модель, управляемая событиями, соответствует ситуациям, когда определенное событие запускает определенное действие. Например, получение входящего сообщения устройства, которое активирует хранилище для последующего использования, или обновление базы данных вызывает дальнейшую обработку.

Чтобы помочь вам исследовать бессерверные технологии Azure в Azure, корпорация Майкрософт разработала и протестировала бессерверное приложение, использующее функции Azure. В этой статье рассматривается код для решения бессерверных функций, а также рассматриваются решения по проектированию, сведения о реализации и некоторые «проблемы», которые могут возникнуть.

Изучение решения

Решение из двух частей описывает гипотетическую систему доставки помощью дронов. Дроны отправляют данные о состоянии полета в облако, где эти сообщения хранятся для последующего использования. Веб-приложение позволяет пользователям получать сообщения для получения последнего состояния устройств.

Вы можете скачать код для этого решения с сайта GitHub.

В этом пошаговом руководстве предполагается знание следующих технологий:

Не обязательно досконально разбираться в функциях или концентраторах событий, но вы должны в общих чертах понимать их возможности. Ниже приведены несколько ресурсов, которые помогут вам начать работу.

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

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

Компания Fabrikam содержит парк дронов для службы доставки с помощью дронов. Приложение состоит из двух основных функциональных областей.

  • Прием событий. Во время полета дроны сообщают о своем состоянии в облачную конечную точку. Приложение принимает и обрабатывает эти сообщения и записывает результаты в серверную базу данных (Cosmos DB). Устройства отправляют сообщения в формате буфера протокола (protobuf). Protobuf — это эффективный самоописывающий формат сериализации.

    Эти сообщения содержат частичные обновления. Через фиксированные промежутки времени каждый дрон отправляет сообщение "ключевого кадра", содержащее все поля состояния. Сообщения о состоянии между ключевыми кадрами содержат только поля, которые изменились с момента последнего сообщения. Такое поведение является типичным для многих устройств Интернета вещей, которым необходимо экономить пропускную способность и мощность.

  • Веб-приложение. Веб-приложение позволяет пользователям искать устройства и запрашивать последнее известное состояние устройства. Пользователи должны войти в приложение и пройти проверку подлинности с помощью Azure Active Directory (Azure AD). Приложение разрешает отправлять запросы только пользователям, которые имеют право доступа к приложению.

Ниже приведен снимок экрана веб-приложения с результатом запроса.

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

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

Компания Fabrikam решила использовать функции Azure для реализации бизнес-логики приложения. Функции Azure — это пример "функций как услуги" (FaaS). В этой вычислительной модели функция — это часть кода, которая развертывается в облаке и выполняется в среде размещения. Эта среда размещения полностью отделяет серверы, на которых выполняется код.

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

Бессерверная архитектура с Функциями Azure является примером управляемой событиями архитектуры. Код функции активируется по некоторому событию, которое этом случае для функции — является внешним — это либо сообщение с дрона, либо HTTP-запрос из клиентского приложения. При использовании приложения-функции вам не нужно писать код для триггера. Вы пишете только код, который выполняется в ответ на триггер. Это означает, что вы можете сосредоточиться на бизнес-логике вместо написания большого объема кода для обработки особенностей инфраструктуры, например системы обмена сообщениями.

Использование бессерверной архитектуры также дает некоторые оперативные преимущества:

  • нет необходимости управлять серверами;
  • вычислительные ресурсы динамически распределяются по мере необходимости;
  • вы платите только за вычислительные ресурсы, которые используются для выполнения кода.
  • вычислительные ресурсы масштабируются по требованию в зависимости от трафика.

Architecture

На схеме ниже в общих чертах показана архитектура приложения.

Схема, на которой показана высокоуровневая архитектура приложения бессерверных функций.

В одном потоке данных стрелки отображают сообщения, передаваемые с устройств в концентраторы событий, и запускают приложение функции. В приложении на одной стрелке показаны недоставленные сообщения, отправляемые в очередь хранилища, а на другой — запись в Azure Cosmos DB. В другом потоке данных на стрелках показано, как клиентское веб-приложение получает статические файлы из статического веб-размещения хранилища больших двоичных объектов через сеть CDN. Другая стрелка показывает клиентский HTTP-запрос, идущий через Управление API. В консоли управления API одна стрелка показывает приложение-функцию, запускающее и считывающее данные из Azure Cosmos DB. Другая стрелка показывает проверку подлинности с помощью Azure AD. Пользователь также входит в Azure AD.

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

  1. сообщения с дронов принимаются Центрами событий Azure;
  2. Центры событий формируют поток событий, содержащий данные сообщений;
  3. эти события активируют приложение Функций Azure для их обработки;
  4. результаты сохраняются в Cosmos DB.

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

  1. статические файлы обслуживаются сетью доставки содержимого Microsoft Azure из хранилища BLOB-объектов;
  2. пользователь входит в веб-приложение с помощью Azure AD;
  3. служба Управления API Azure выступает в качестве шлюза, предоставляющего конечную точку REST API;
  4. HTTP-запросы от клиента активируют приложение Функций Azure, которое считывает данные из Cosmos DB и возвращает результат.

Это приложение основано на двух эталонных архитектурах, соответствующих двум функциональным блокам, описанным выше:

Вы можете прочитать эти статьи, чтобы получить дополнительные сведения об архитектуре, службах Azure, которые используются в решении, а также рекомендации по масштабируемости, безопасности и надежности.

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

Давайте начнем с рассмотрения функции, которая обрабатывает сообщения с дронов из Центров событий. Функция определена в классе с именем 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;
        }
    }
    ...
}

Этот класс имеет несколько зависимостей, которые добавляются в конструктор с помощью внедрения зависимости.

  • Интерфейсы ITelemetryProcessor и IStateChangeProcessor определяют два вспомогательных объекта. Как мы увидим, эти объекты выполняют большую часть работы.

  • TelemetryClient — это часть пакета SDK Application Insights. Он используется для отправки пользовательских метрик приложения в Application Insights.

Позже мы рассмотрим способы настройки внедрения зависимостей. Пока просто считайте, что они существуют.

Настройка триггера Центров событий

Логика в функции реализуется как асинхронный метод с именем RunAsync. Вот так выглядит подпись метода.

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

Метод принимает следующие параметры.

  • messages — массив сообщений концентратора событий.
  • deadLetterMessages — очередь службы хранилища Azure, используемая для хранения недоставленных сообщений.
  • logging — интерфейс ведения журнала для записи журналов приложения. Эти журналы отправляются в Azure Monitor.

Атрибут EventHubTrigger параметра messages настраивает триггер. Свойства атрибута определяют имя концентратора событий, строку подключения и группу потребителей. (Группа потребителей — изолированное представление потоковой передачи событий Центра событий. Эта абстракция позволяет использовать несколько потребителей того же концентратора событий.)

Обратите внимание на знаки процента (%) в некоторых свойствах атрибута. Они указывают на то, что свойство задает имя параметра приложения, а фактическое значение извлекается из такого параметра приложения во время выполнения. В противном случае, без знаков процента, свойство передает буквальное значение.

Свойство Connection — исключение. Это свойство всегда указывает имя параметра приложения, а не буквальное значение, поэтому знак процента не требуется. Это различие связано с тем, что строка подключения является секретной и ее нельзя содержать в исходном коде.

Хотя другие два свойства (имя концентратора событий, а также группа потребителей) не являются конфиденциальными данными, как например строка подключения, их все же лучше поместить в параметры приложения, а не прописывать в коде. Таким образом их можно обновлять без перекомпиляции приложения.

Дополнительные сведения о настройке этого триггера см. в разделе Привязки Центров событий Azure для Функций Azure.

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

Вот реализация метода RawTelemetryFunction.RunAsync, который обрабатывает пакет сообщений:

[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 содержит массив сообщений из концентратора событий. Пакетная обработка сообщений обычно эффективнее, чем чтение сообщений по одному. Однако вы должны убедиться, что функция устойчива и корректно обрабатывает сбои и исключения. Иначе, если функция выдает необработанное исключение в середине пакета, вы можете потерять оставшиеся сообщения. Этот момент более подробно рассматривается в разделе Обработка ошибок.

Но если вы игнорируете обработку исключений, логика обработки каждого сообщения проста:

  1. Вызовите ITelemetryProcessor.Deserialize для десериализации сообщения, содержащего изменение состояния устройства.
  2. Вызовите IStateChangeProcessor.UpdateState для обработки изменения состояния.

Давайте подробнее рассмотрим эти два метода, начиная с метода Deserialize.

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

Метод TelemetryProcess.Deserialize принимает массив байтов, который содержит полезные данные сообщения. Он десериализует эти полезные данные и возвращает объект DeviceState, который соответствует состоянию беспилотника. Состояние может представлять собой частичное обновление, содержащее только отличия от последнего известного состояния. Поэтому метод должен обрабатывать поля null в десериализованных полезных данных.

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> для десериализации необработанного сообщения. Затем результаты преобразуются в модель POCO, с которой легче работать. Этот конструктор помогает изолировать логику обработки от деталей реализации сериализации. Интерфейс ITelemetrySerializer<T> определен в общей библиотеке, которая также используется симулятором устройства для создания событий имитируемого устройства и отправки их в концентраторы событий.

using System;

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

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

Метод UpdateState

Метод StateChangeProcessor.UpdateState применяет изменения состояния. Последнее известное состояние для каждого беспилотника сохраняется как документ JSON в Cosmos DB. Поскольку беспилотники отправляют частичные обновления, приложение не может просто перезаписать документ во время получения обновления. Вместо этого ему нужно извлечь предыдущее состояние, объединить поля и затем выполнить операцию upsert.

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

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

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

        DeviceState target = null;

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

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

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

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

Этот код использует интерфейс IDocumentClient для получения документа из Cosmos DB. Если документ существует, новые значения состояния объединяются в существующий документ. В противном случае создается новый документ. Оба случая обрабатываются методом UpsertDocumentAsync.

Этот код оптимизирован для случая, когда документ уже существует и может быть объединен. В первом сообщении телеметрии от данного беспилотника метод ReadDocumentAsync вызовет исключение, поскольку для этого беспилотника нет документа. После первого сообщения документ уже будет доступен.

Обратите внимание, что этот класс использует внедрение зависимостей для внедрения IDocumentClient для Cosmos DB и IOptions<T> с параметрами конфигурации. Настройка внедрения зависимостей показана далее.

Примечание

Функции Azure поддерживают привязку выходных данных для Cosmos DB. Эта привязка позволяет приложению-функции записывать документы в Cosmos DB без какого-либо кода. Однако выходная привязка не будет работать для этого конкретного сценария из-за необходимости наличия пользовательской логики upsert.

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

Как упоминалось ранее, приложение-функция RawTelemetryFunction обрабатывает пакет сообщений в цикле. Это означает, что функция должна корректно обрабатывать любые исключения и продолжать обработку оставшейся части пакета. Иначе сообщения могут быть отброшены.

Если при обработке сообщения возникает исключение, функция помещает сообщение в очередь недоставленных сообщений:

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

Очередь недоставленных сообщений определяется с помощью выходной привязки к хранилищу очередей:

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

Совет по развертыванию: В шаблоне диспетчер ресурсов, который создает учетную запись хранения, можно автоматически заполнять параметр приложения строкой подключения. Хитрость заключается в использовании функции listKeys .

Вот раздел шаблона, который создает учетную запись хранилища для очереди:

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

Вот раздел шаблона, который создает функцию приложения.


    {
        "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. Важно сделать ресурс приложения-функции зависимым от ресурса учетной записи хранилища (см. элемент dependsOn). Это гарантирует, что учетная запись хранилища будет создана первой, а строка подключения будет доступна.

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

Следующий код настраивает внедрение зависимости для функции RawTelemetryFunction:

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

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

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

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

Функции Azure, написанные для .NET, могут использовать платформу внедрения зависимостей ASP.NET Core. Основная идея заключается в том, что вы объявляете метод запуска для своей сборки. Метод принимает интерфейс IFunctionsHostBuilder, который используется для объявления зависимостей для DI. Это можно сделать, вызвав метод Add* объекта Services. Когда вы добавляете зависимость, то указываете время ее существования:

  • Промежуточные объекты создаются каждый раз, когда их запрашивают.
  • Объекты с заданной областью создаются один раз во время выполнение функции.
  • Отдельные объекты повторно используются при выполнении функций в течение времени существования узла функции.

В этом примере объекты TelemetryProcessor и StateChangeProcessor объявляются как промежуточные. Это применимо для простых служб без отслеживания состояния. С другой стороны, чтобы достичь оптимальной производительности, класс DocumentClient должен быть отдельным. Дополнительные сведения см. в статье о советах по повышению производительности в Azure Cosmos DB и .NET.

Если вернуться к коду для RawTelemetryFunction, вы увидите другую зависимость, которая не отображается в коде настройки DI, а именно класс TelemetryClient, используемый для метрики журнала приложения. Среда выполнения функций автоматически регистрирует этот класс в контейнер DI, так что он не требует явной регистрации.

Дополнительные сведения о DI в Функциях Azure см. в следующих статьях:

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

Иногда для объекта требуется инициализация с помощью некоторых значений конфигурации. Как правило, эти параметры должны исходить из настроек приложения или (при использовании секретов) из хранилища ключей Azure.

В случае этого приложения, есть два примера. Первый — класс DocumentClient берет конечную точку службы Cosmos DB и ключ. Для этого объекта приложение регистрирует лямбда-выражение, которое будет вызываться с помощью контейнера DI. Это лямбда-выражение использует интерфейс IConfiguration для чтения значений конфигурации:

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. Для этого объекта мы используем подход, который называется шаблон параметров. Это работает следующим образом:

  1. Определите класс T, который содержит параметры конфигурации. В данном случае — имя базы данных Cosmos DB и имя коллекции.

    public class StateChangeProcessorOptions
    {
        public string COSMOSDB_DATABASE_NAME { get; set; }
        public string COSMOSDB_DATABASE_COL { get; set; }
    }
    
  2. Добавьте класс T в качестве класса параметров для DI.

    builder.Services.AddOptions<StateChangeProcessorOptions>()
        .Configure<IConfiguration>((configSection, configuration) =>
        {
            configuration.Bind(configSection);
        });
    
  3. В конструктор класса, который настраивается, включите параметр IOptions<T>.

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

Система DI автоматически заполнит класс параметров, используя значения конфигурации, и передаст его в конструктор.

У этого подхода есть несколько преимуществ:

  • отделение класса от источника значений конфигурации;
  • легкая настройка разных источников конфигурации, таких как переменные среды или файлы конфигурации JSON;
  • упрошенное модульное тестирование;
  • использование строго типизированных параметров класса, которые имеют меньшую вероятность появления ошибок, чем при передаче скалярных значений.

Функция GetStatus

В другом приложении Функций Azure реализован простой интерфейс REST API для получения последнего известного состояния дрона в этом решении. Эта функция определена в классе с именем GetStatusFunction. Ниже приведен полный код для функции.

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. Функция использует входную привязку Cosmos DB, чтобы получить запрашиваемый документ. Следует учитывать, что эта привязка будет выполнятся до выполнения логики авторизации внутри функции. Если неавторизованный пользователь запрашивает документ, привязка функции по-прежнему будет получать его. Затем код авторизации возвратит 401, поэтому пользователь не увидит документ. Соответствие данной реакции на событие может зависеть от требований. Например, этот подход может усложнить доступ к конфиденциальным данным при аудите данных.

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

Веб-приложение использует Azure AD для выполнения проверки подлинности пользователей. Так как приложение является одностраничным приложением, которое выполняется в браузере, поток неявного предоставления разрешения подходит для:

  1. перенаправления веб-приложением пользователя к поставщику удостоверений (в данном случае Azure AD);
  2. ввода пользователем своих учетных данных;
  3. перенаправления поставщиком удостоверений веб-приложения с помощью маркера доступа;
  4. отправки запроса веб-приложением в веб-API и включения маркера доступа в заголовок Авторизации.

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

Приложение Функции можно настроить для проверки подлинности пользователей без кода. Дополнительные сведения см. в статье Проверка подлинности и авторизация в службе приложений Azure.

С другой стороны, для авторизации обычно требуется бизнес-логика. Azure AD поддерживает проверку подлинности на основе утверждений. В этой модели удостоверение пользователя представлено как набор утверждений, которые поступают от поставщика удостоверений. Утверждением может быть любой фрагмент сведений о пользователе, например его имя или адрес электронной почты.

Маркер доступа содержит подмножество утверждений пользователей. Среди них — любые роли приложения, которым назначен пользователь.

Параметр principal функции является объектом ClaimsPrincipal, который содержит утверждения из маркера доступа. Каждое утверждение являет собой пару ключ/значение типа и значения утверждения. Они используются приложением для авторизации запроса.

Следующий метод расширения проверяет, содержит ли объект ClaimsPrincipal набор ролей. Он возвращает false, если любая из указанных ролей отсутствует. Если этот метод возвращает значение false, функция возвращает код HTTP 401 (недостаточно прав).

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

Дополнительные сведения о проверке подлинности и авторизации в этом приложении см. в разделе Вопросы безопасности эталонной архитектуры.

Следующие шаги

Получив представление о том, как работает это эталонное решение, ознакомьтесь с рекомендациями и рекомендациями по аналогичным решениям.

Функции Azure — это всего лишь один вариант вычислений Azure. Сведения о выборе технологии вычислений см. в статье Выбор службы вычислений Azure для приложения.