Návod k kódu: Bezserverová aplikace se službou Functions

Azure Event Hubs
Azure Functions

Bezserverové modely abstrahuje kód ze základní výpočetní infrastruktury, což vývojářům umožňuje soustředit se na obchodní logiku bez rozsáhlého nastavení. Bezserverový kód snižuje náklady, protože platíte jenom za prostředky provádění kódu a dobu trvání.

Model řízený bezserverovou událostí odpovídá situacím, kdy určitá událost aktivuje definovanou akci. Příjem příchozí zprávy zařízení například aktivuje úložiště pro pozdější použití nebo aktualizace databáze aktivuje některé další zpracování.

Microsoft vyvinul a otestoval bezserverovou aplikaci, která používá Azure Functions, aby vám pomohla prozkoumat bezserverové technologie v Azure. Tento článek vás provede kódem pro řešení bezserverových funkcí a popisuje rozhodnutí o návrhu, podrobnosti implementace a některé "gotchas", se kterými se můžete setkat.

Prozkoumejte toto řešení

Dvoudílné řešení popisuje hypotetický systém doručování dronů. Drony odesílají stav letu do cloudu, který tyto zprávy ukládá pro pozdější využití. Webová aplikace umožňuje uživatelům načíst zprávy, aby získali nejnovější stav zařízení.

Kód pro toto řešení si můžete stáhnout z GitHubu.

Tento názorný postup předpokládá základní znalost následujících technologií:

Nemusíte být odborníkem na službu Functions ani Event Hubs, ale měli byste rozumět jejich funkcím na vysoké úrovni. Tady je několik vhodných zdrojů informací, které vám pomůžou začít:

Vysvětlení scénáře

Diagram of the functional blocks

Společnost Fabrikam spravuje flotilu dronů pro službu doručování pomocí dronů. Aplikace se skládá ze dvou hlavních funkčních oblastí:

  • Příjem událostí. Drony během letu odesílají stavové zprávy do koncového bodu cloudu. Aplikace tyto zprávy ingestuje a zpracovává a zapisuje výsledky do back-endové databáze (Azure Cosmos DB). Zařízení odesílají zprávy ve formátu protobuf (vyrovnávací paměť protokolu). Protobuf je efektivní, samostatný formát serializace.

    Tyto zprávy obsahují částečné aktualizace. Každý dron pravidelně odesílá zprávy klíčových snímků, které obsahují všechna pole stavu. Stavové zprávy mezi klíčovými snímky zahrnují pouze pole, která se změnila od poslední zprávy. Toto chování je typické pro řadu zařízení IoT, která potřebují šetřit šířku pásma a výkon.

  • Webová aplikace. Webová aplikace umožnuje uživatelům vyhledat zařízení a dotazovat poslední známý stav zařízení. Uživatelé se musí přihlásit k aplikaci a ověřit pomocí Microsoft Entra ID. Aplikace umožňuje požadavky pouze od uživatelů, kteří mají oprávnění pro přístup k aplikaci.

Tady je snímek obrazovky webové aplikace s výsledkem dotazu:

Screenshot of client app

Návrh aplikace

Společnost Fabrikam se rozhodla využít Azure Functions k implementaci obchodní logiky aplikace. Azure Functions je příkladem funkcí jako služby (FaaS). V tomto výpočetním modelu je funkce kus kódu, který je nasazený do cloudu a běží v hostitelském prostředí. Toto hostitelské prostředí kompletně abstrahuje servery, na kterých se kód spouští.

Proč zvolit bezserverový přístup?

Bezserverová architektura se službou Functions je příkladem architektury založené na událostech. Kód funkce je aktivační událostí, která je pro funkci externí – v tomto případě buď zpráva z dronu, nebo požadavek HTTP z klientské aplikace. Díky aplikaci funkcí nemusíte psát žádný kód pro trigger. Napíšete pouze kód, který se spustí v reakci na trigger. To znamená, že se můžete soustředit na obchodní logiku a nemusíte psát velké množství kódu, který by se staral o záležitosti infrastruktury, jako je zasílání zpráv.

Použití bezserverové architektury s sebou nese také několik provozních výhod:

  • Není potřeba spravovat servery.
  • Výpočetní prostředky se přidělují dynamicky podle potřeby.
  • Účtují se vám pouze výpočetní prostředky využité ke spuštění vašeho kódu.
  • Výpočetní prostředky se škálují na vyžádání v závislosti na provozu.

Architektura

Následující diagram ukazuje základní architekturu aplikace:

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

V jednom toku dat zobrazují šipky zprávy ze zařízení do event Hubs a aktivují aplikaci funkcí. Jedna šipka z aplikace zobrazuje zprávy s nedoručenou zprávou, které se přejdou do fronty úložiště, a další šipka ukazuje zápis do služby Azure Cosmos DB. V jiném toku dat šipky ukazují, že klientská webová aplikace získává statické soubory ze statického hostování webu úložiště objektů blob prostřednictvím sítě CDN. Další šipka ukazuje požadavek HTTP klienta procházející službou API Management. Jedna šipka ze služby API Management zobrazuje trigger aplikace funkcí a čte data ze služby Azure Cosmos DB. Další šipka ukazuje ověřování prostřednictvím ID Microsoft Entra. Uživatel se také přihlásí k Microsoft Entra ID.

Příjem událostí:

  1. Azure Event Hubs přijme zprávy dronů.
  2. Event Hubs vytvoří stream událostí, které obsahují data zpráv.
  3. Tyto události aktivují aplikaci Azure Functions, která je zpracuje.
  4. Výsledky se ukládají ve službě Azure Cosmos DB.

Webová aplikace:

  1. Statické soubory poskytuje síť CDN z úložiště objektů blob.
  2. Uživatel se k webové aplikaci přihlásí pomocí ID Microsoft Entra.
  3. Azure API Management funguje jako brána, která zveřejňuje koncový bod rozhraní REST API.
  4. Požadavky HTTP z klienta aktivují aplikaci Azure Functions, která čte ze služby Azure Cosmos DB a vrací výsledek.

Tato aplikace je založená na dvou referenčních architekturách, které odpovídají dvěma funkčním blokům popsaným výše:

V těchto článcích najdete další informace o základní architektuře a službách Azure použitých v řešení a důležité informace týkající se škálovatelnosti, zabezpečení a spolehlivosti.

Funkce zpracovávající telemetrii z dronů

Začněme tím, že se podíváme na funkci, která zpracovává zprávy z dronů ze služby Event Hubs. Tato funkce je definovaná ve třídě 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;
        }
    }
    ...
}

Tato třída má několik závislostí, které se pomocí injektáže závislostí vkládají do konstruktoru:

  • Rozhraní ITelemetryProcessor a IStateChangeProcessor definují dva pomocné objekty. Jak si ukážeme, tyto objekty se starají o většinu práce.

  • Rozhraní TelemetryClient je součástí sady Application Insights SDK. Slouží k odesílání vlastních metrik aplikací do služby Application Insights.

Později se podíváme na to, jak nakonfigurovat injektáž závislostí. Prozatím stačí vědět, že tyto závislosti existují.

Konfigurace triggeru služby Event Hubs

Logika ve funkci se implementuje jako asynchronní metoda RunAsync. Tady je podpis této metody:

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

Tato metoda přijímá následující parametry:

  • messages je pole zpráv z centra událostí.
  • deadLetterMessages je fronta Azure Storage sloužící k ukládání nedoručených zpráv.
  • logging poskytuje rozhraní protokolování pro zápis do aplikačních protokolů. Tyto protokoly se odesílají do služby Azure Monitor.

Atributem EventHubTrigger parametru messages se konfiguruje trigger. Ve vlastnostech tohoto atributu se určuje název centra událostí, připojovací řetězec a skupina uživatelů. (Skupina příjemců je izolované zobrazení streamu událostí služby Event Hubs. Tato abstrakce umožňuje více příjemcům stejného centra událostí.)

Všimněte si symbolu procent (%) v některých vlastnostech atributu. Tyto symboly značí, že se ve vlastnosti uvádí název nastavení aplikace a skutečná hodnota se přebírá z příslušného nastavení aplikace za běhu. Vlastnosti bez symbolu procent jinak uvádí literálové hodnoty.

Výjimkou je vlastnost Connection. Tato vlastnost vždy udává název nastavení aplikace, a nikdy literálovou hodnotu, proto symbol procent není potřeba. Důvodem tohoto rozdílu je, že připojovací řetězec je tajný a nikdy by se neměl uvádět ve zdrojovém kódu.

Přestože zbývající dvě vlastnosti (název centra událostí a skupina uživatelů) neobsahují citlivá data jako připojovací řetězec, místo jejich pevného zakódování je stále lepší je umístit do nastavení aplikace. Díky tomu je bude možné aktualizovat bez nutnosti rekompilace aplikace.

Další informace o konfiguraci tohoto triggeru najdete v tématu Vazby služby Azure Event Hubs pro Azure Functions.

Logika zpracování zpráv

Tady je implementace metody RawTelemetryFunction.RunAsync, která zpracovává dávky zpráv:

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

Při vyvolání této funkce parametr messages obsahuje pole zpráv z centra událostí. Zpracováním zpráv v dávkách se obvykle dosahuje lepšího výkonu než při postupném čtení jednotlivých zpráv. Je však potřeba zajistit, že je funkce odolná a dokáže zpracovat selhání a výjimky bez výpadku. V opačném případě, pokud funkce uprostřed dávky vyvolá neošetřenou výjimku, může dojít ke ztrátě zbývajících zpráv. Tento aspekt je podrobněji popsaný v části Zpracování chyb.

Pokud ale budete zpracování výjimek ignorovat, logika zpracování jednotlivých zpráv je jednoduchá:

  1. Zavolá se metoda ITelemetryProcessor.Deserialize, která deserializuje zprávu obsahující změnu stavu zařízení.
  2. Zavolá se metoda IStateChangeProcessor.UpdateState, která zpracuje změnu stavu.

Podívejme se na tyto dvě metody podrobněji a začněme metodou Deserialize.

Metoda Deserialize

Metoda TelemetryProcess.Deserialize přijímá pole bajtů obsahující datovou část zprávy. Tuto datovou část deserializuje a vrátí objekt DeviceState, který představuje stav dronu. Tento stav může představovat částečnou aktualizaci a obsahovat pouze rozdíly od posledního známého stavu. Metoda proto musí být schopná zpracovat pole null v deserializované datové části.

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

Tato metoda k deserializaci nezpracovaných zpráv využívá další pomocné rozhraní ITelemetrySerializer<T>. Výsledky se pak transformují do modelu POCO, se kterým se lépe pracuje. Tento návrh pomáhá izolovat logiku zpracování od podrobné implementace serializace. Rozhraní ITelemetrySerializer<T> je definované ve sdílené knihovně, kterou využívá také simulátor zařízení ke generování událostí simulovaného zařízení a jejich odesílání do služby Event Hubs.

using System;

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

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

Metoda UpdateState

Metoda StateChangeProcessor.UpdateState používá změny stavu. Poslední známý stav každého dronu se uloží jako dokument JSON ve službě Azure Cosmos DB. Vzhledem k tomu, že drony odesílají částečné aktualizace, aplikace po přijetí aktualizace nemůže dokument jednoduše přepsat. Místo toho musí načíst předchozí stav, sloučit pole a pak provést operaci 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);
    }
}

Tento kód používá IDocumentClient rozhraní k načtení dokumentu ze služby Azure Cosmos DB. Pokud dokument existuje, nové hodnoty stavu se sloučí s existujícím dokumentem. V opačném případě se vytvoří nový dokument. O zpracování obou případů se stará metoda UpsertDocumentAsync.

Tento kód je optimalizovaný pro případ, kdy dokument již existuje a je možné s ním provést sloučení. U první telemetrické zprávy z každého dronu metoda ReadDocumentAsync vyvolá výjimku, protože pro daný dron ještě neexistuje žádný dokument. Po první zprávě už bude dokument k dispozici.

Všimněte si, že tato třída používá injektáž závislostí k vložení IDocumentClient služby Azure Cosmos DB a IOptions<T> nastavení konfigurace. Nastavení injektáže závislostí si ukážeme později.

Poznámka

Azure Functions podporuje výstupní vazbu pro službu Azure Cosmos DB. Tato vazba umožňuje aplikaci funkcí psát dokumenty ve službě Azure Cosmos DB bez jakéhokoli kódu. V tomto konkrétním scénáři však výstupní vazba nebude fungovat, protože je potřeba vlastní logika operace upsert.

Zpracování chyb

Jak už bylo zmíněno dříve, aplikace funkcí RawTelemetryFunction zpracovává ve smyčce dávky zpráv. To znamená, že funkce musí být schopná bez výpadku zpracovat všechny výjimky a pokračovat ve zpracovávání zbytku dávky. Jinak může dojít k zahození některých zpráv.

Pokud při zpracování zprávy dojde k výjimce, funkce danou zprávu umístí do fronty nedoručených zpráv:

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

Fronta nedoručených zpráv se definuje pomocí výstupní vazby k frontě úložiště:

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

Výstupní vazbu zde určuje atribut Queue a atribut StorageAccount určuje název nastavení aplikace, které obsahuje připojovací řetězec pro účet úložiště.

Tip k nasazení: V šabloně Resource Manageru, která vytvoří účet úložiště, můžete automaticky naplnit nastavení aplikace připojovací řetězec. Trikem je použití funkce listKeys .

Tady je část šablony, která pro frontu vytváří účet úložiště:

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

Tady je část šablony, která vytváří aplikaci funkcí.


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

Tato část definuje nastavení aplikace DeadLetterStorage, jehož hodnota se vyplní pomocí funkce listKeys. Prostředek aplikace funkcí je důležité nastavit jako závislý na prostředku účtu úložiště (viz element dependsOn). Tím se zajistí, že se účet úložiště vytvoří jako první a že bude k dispozici připojovací řetězec.

Nastavení injektáže závislostí

Následující kód nastaví injektáž závislostí pro funkci 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);
            });
        }
    }
}

Aplikace Azure Functions napsané pro .NET můžou využít architekturu injektáže závislostí v ASP.NET Core. Základní myšlenka je taková, že pro své sestavení deklarujete spouštěcí metodu. Tato metoda přebírá rozhraní IFunctionsHostBuilder, které slouží k deklaraci závislostí pro injektáž závislostí. Provedete do voláním metody Add* pro objekt Services. Při přidávání závislosti zadáte její dobu života:

  • Přechodné objekty se vždy vytvářejí na vyžádání.
  • Vymezené objekty se při každém spuštění funkce vytvoří pouze jednou.
  • Jednoúčelové objekty se po dobu života hostitele funkce používají opakovaně pro různá spuštění funkce.

V tomto případě jsou objekty TelemetryProcessor a StateChangeProcessor deklarované jako přechodné. To je vhodné pro jednoduché bezstavové služby. Třída DocumentClient by na druhou stranu měla být jednoúčelová, aby se zajistil do nejlepší výkon. Další informace najdete v tématu Tipy pro zvýšení výkonu pro Azure Cosmos DB a .NET.

Pokud se vrátíte ke kódu funkce RawTelemetryFunction, uvidíte, že obsahuje další závislost, která není v kódu pro nastavení injektáže závislostí. Konkrétně se jedná o třídu TelemetryClient, která slouží k protokolování metrik aplikace. Modul runtime služby Functions tuto třídu automaticky zaregistruje do kontejneru injektáže závislostí, takže ji nemusíte registrovat explicitně.

Další informace o injektáži závislostí ve službě Azure Functions najdete v následujících článcích:

Předávání nastavení konfigurace v injektáži závislostí

Objekty je někdy potřeba inicializovat s určitými hodnotami konfigurace. Obecně platní, že by tato nastavení měla pocházet z nastavení aplikace nebo (v případě tajných kódů) ze služby Azure Key Vault.

Tato aplikace obsahuje dva příklady. Nejprve třída DocumentClient přebírá koncový bod služby a klíč služby Azure Cosmos DB. Aplikace pro tento objekt zaregistruje operátor lambda, který se vyvolá kontejnerem injektáže závislostí. Tento operátor lambda ke čtení hodnot konfigurace využívá rozhraní 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);
});

Druhým příkladem je třída StateChangeProcessor. U tohoto objektu používáme přístup označovaný jako vzor možností. Jak to funguje:

  1. Definuje se třída T, která obsahuje vaše nastavení konfigurace. V tomto případě název databáze a název kolekce Azure Cosmos DB.

    public class StateChangeProcessorOptions
    {
        public string COSMOSDB_DATABASE_NAME { get; set; }
        public string COSMOSDB_DATABASE_COL { get; set; }
    }
    
  2. Třída T se přidá jako třída možností pro injektáž závislostí.

    builder.Services.AddOptions<StateChangeProcessorOptions>()
        .Configure<IConfiguration>((configSection, configuration) =>
        {
            configuration.Bind(configSection);
        });
    
  3. Do konstruktoru konfigurované třídy se vloží parametr IOptions<T>.

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

Systém injektáže závislostí automaticky vyplní třídu možností hodnotami konfigurace a předá ji do konstruktoru.

Tento přístup nabízí několik výhod:

  • Oddělení třídy od zdroje hodnot konfigurace
  • Snadné nastavení různých zdrojů konfigurace, jako jsou například proměnné prostředí nebo konfigurační soubory JSON
  • Zjednodušení testování částí
  • Použití třídy možností se silnou typovou kontrolou, která je méně náchylná k chybám než pouhé předávání skalárních hodnot

Funkce GetStatus

Další aplikace funkcí v tomto řešení implementuje jednoduché rozhraní REST API, které umožňuje získat poslední známý stav dronu. Tato funkce je definovaná ve třídě GetStatusFunction. Tady je kompletní kód této funkce:

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

Tato funkce využívá trigger HTTP ke zpracování požadavku HTTP GET. Funkce používá vstupní vazbu Azure Cosmos DB k načtení požadovaného dokumentu. Jedním z důležitých aspektů je, že se tato vazba spustí před provedením logiky ověřování uvnitř funkce. Pokud si dokument vyžádá neoprávněný uživatel, vazba funkce přesto dokument načte. Pak se vrátí ověřovací kód 401, takže se dokument uživateli nezobrazí. Jestli je toto chování přijatelné může záviset na vašich požadavcích. Tento přístup například může ztížit audit přístupu k citlivým datům.

Ověřování a autorizace

Webová aplikace používá k ověřování uživatelů ID Microsoft Entra. Vzhledem k tomu, že se jedná o jednostránkovou aplikaci (SPA) v prohlížeči, je vhodný tok implicitního udělení oprávnění:

  1. Webová aplikace přesměruje uživatele na zprostředkovatele identity (v tomto případě ID Microsoft Entra).
  2. Uživatel zadá své přihlašovací údaje.
  3. Zprostředkovatel identity uživatele přesměruje zpět do webové aplikace s přístupovým tokenem.
  4. Webová aplikace odešle požadavek na webové rozhraní API a v autorizační hlavičce uvede přístupový token.

Implicit flow diagram

V aplikaci funkcí je možné nakonfigurovat ověřování uživatelů bez jakéhokoli kódu. Další informace najdete v tématu Ověřování a autorizace ve službě Azure App Service.

Autorizace na druhou stranu obvykle vyžaduje určitou obchodní logiku. Microsoft Entra ID podporuje ověřování na základě deklarací identity. V tomto modelu identitu uživatele představuje sada deklarací identity, které pocházejí od zprostředkovatele identity. Deklarací identity může být jakákoli informace o uživateli, například jeho jméno nebo e-mailová adresa.

Přístupový token obsahuje podmnožinu deklarací identity uživatele. Patří mezi ně i všechny aplikační role, ke kterým je uživatel přiřazený.

Parametr principal funkce je objektem ClaimsPrincipal, který obsahuje deklarace identity z přístupového tokenu. Každá deklarace identity je párem klíč-hodnota obsahujícím typ a hodnotu deklarace identity. Aplikace pomocí nich autorizuje požadavky.

Následující rozšiřující metoda testuje, jestli objekt ClaimsPrincipal obsahuje sadu rolí. Pokud některá z uvedených rolí chybí, vrátí hodnotu false. Pokud tato metoda vrátí hodnotu false, funkce vrátí chybu HTTP 401 (Neautorizováno).

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

Další informace o ověřování a autorizaci v této aplikaci najdete v části Aspekty zabezpečení referenční architektury.

Další kroky

Jakmile zjistíte, jak toto referenční řešení funguje, seznamte se s osvědčenými postupy a doporučeními pro podobná řešení.

Azure Functions je jen jedna možnost azure compute. Nápovědu k výběru výpočetní technologie najdete v tématu Volba výpočetní služby Azure pro vaši aplikaci.