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

Центры событий Azure
Функции Azure

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

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

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

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

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

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

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

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

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

Diagram of the functional blocks

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

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

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

  • Веб-приложение. Веб-приложение позволяет пользователям искать устройства и запрашивать последнее известное состояние устройства. Пользователи должны войти в приложение и пройти проверку подлинности с помощью идентификатора Microsoft Entra. Приложение разрешает отправлять запросы только пользователям, которые имеют право доступа к приложению.

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

Screenshot of client app

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

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

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

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

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

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

Архитектура

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

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

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

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

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

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

  1. статические файлы обслуживаются сетью доставки содержимого Microsoft Azure из хранилища BLOB-объектов;
  2. Пользователь входит в веб-приложение с помощью идентификатора Microsoft Entra.
  3. служба Управления API Azure выступает в качестве шлюза, предоставляющего конечную точку REST API;
  4. HTTP-запросы от клиента активируют Функции Azure приложение, которое считывает из 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 в Azure 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 интерфейс для получения документа из Azure Cosmos DB. Если документ существует, новые значения состояния объединяются в существующий документ. В противном случае создается новый документ. Оба случая обрабатываются методом UpsertDocumentAsync.

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

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

Примечание.

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

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

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

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

Implicit flow diagram

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

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

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

Параметр 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. Сведения о выборе технологии вычислений см. в этой статье.