Kódbemutató: Kiszolgáló nélküli alkalmazás a Functions használatával

Azure Event Hubs
Azure Functions

A kiszolgáló nélküli modellek absztrakciós kódját a mögöttes számítási infrastruktúra teszi lehetővé, hogy a fejlesztők átfogó beállítás nélkül összpontosítsanak az üzleti logikára. A kiszolgáló nélküli kód csökkenti a költségeket, mivel csak a kódvégrehajtási erőforrásokért és az időtartamért kell fizetnie.

A kiszolgáló nélküli eseményvezérelt modell megfelel azoknak a helyzeteknek, amikor egy adott esemény egy meghatározott műveletet indít el. A bejövő eszközüzenetek fogadása például elindítja a tárterületet későbbi használatra, vagy egy adatbázis-frissítés további feldolgozást indít el.

Az Azure kiszolgáló nélküli technológiáinak megismeréséhez a Microsoft kifejlesztett és tesztelt egy Azure Functionst használó kiszolgáló nélküli alkalmazást. Ez a cikk végigvezeti a kiszolgáló nélküli Functions-megoldás kódját, és ismerteti a tervezési döntéseket, a megvalósítás részleteit és néhány lehetséges "gotchas"-t.

A megoldás megismerése

A kétrészes megoldás egy hipotetikus drónkézbesítési rendszert ír le. A drónok elküldik a felhőbe a repülési állapotukat, ahol ezek az üzenetek későbbi felhasználásra tárolva lesznek. A webalkalmazások segítségével a felhasználók lekérhetik az üzeneteket az eszközök legújabb állapotának lekéréséhez.

A megoldás kódját a GitHubról töltheti le.

Ez az útmutató a következő technológiák alapszintű ismeretét feltételezi:

Nem kell Functions- vagy az Event Hubs-szakértőnek lennie, de jól kell ismernie a funkcióikat. Hasznos forrásanyagok az első lépésekhez:

A forgatókönyv megismerése

Diagram of the functional blocks

A Fabrikam egy drónokból álló flottát kezel egy drónos házhozszállítási szolgáltatás részeként. Az alkalmazás két fő funkcionális területtel rendelkezik:

  • Eseménybetöltés. Repülés közben a drónok állapotüzeneteket küldenek a felhővégpontokra. Az alkalmazás betölti és feldolgozza ezeket az üzeneteket, és az eredményeket egy háttéradatbázisba (Azure Cosmos DB) írja. Az eszközök üzeneteket küldenek protokollpuffer (protobuf) formátumban. A protobuf egy hatékony, önleíró szerializálási formátum.

    Ezek az üzenetek részleges frissítéseket tartalmaznak. Rögzített időközönként minden egyes drón „kulcskeret” üzenetet küld, amely az összes állapotmezőt tartalmazza. Az állapotüzenetekben csak olyan mezők szerepelnek kulcskeretek között, amelyek az előző üzenet óta változtak. Ez a viselkedés számos olyan IoT-eszközre jellemző, amelynek sávszélességet és teljesítményt kell megtakarítania.

  • Webalkalmazás. A webalkalmazás lehetővé teszi a felhasználóknak, hogy megkeressenek egy eszközt, és lekérdezzék az utolsó ismert állapotát. A felhasználóknak be kell jelentkezniük az alkalmazásba, és hitelesíteni kell magukat a Microsoft Entra-azonosítóval. Az alkalmazás kizárólag azoktól a felhasználóktól fogad el kérelmeket, akik jogosultak az alkalmazás használatára.

Íme egy, a lekérdezés eredményét megjelenítő képernyőkép a webalkalmazásról:

Screenshot of client app

Az alkalmazás megtervezése

A Fabrikam úgy döntött, hogy az Azure Functionst fogja használni az alkalmazás üzleti logikájának implementálásához. Az Azure Functions példa a „szolgáltatott függvényekre” (FaaS). Ebben a számítási modellben a függvény egy kódrészlet, amely a felhőben van üzembe helyezve, és üzemeltetési környezetben fut. Az üzemeltetési környezet teljes mértékben absztrahálja a kódokat futtató kiszolgálókat.

Miért válasszon kiszolgáló nélküli megközelítést?

Az eseményvezérelt architektúrára példa a Functionsszel létrehozott kiszolgáló nélküli architektúra. A függvénykódot egy, a függvényen kívüli esemény aktiválja – ebben az esetben egy drón üzenete vagy egy ügyfélalkalmazás HTTP-kérése. Függvényalkalmazással Önnek nem kell kódot írnia a triggerhez. Csak a triggerre adott válasz kódját kell megírnia. Ez azt jelenti, hogy az üzleti logikára összpontosíthat, ahelyett, hogy sok kódot kellene írnia az infrastrukturális funkciók, például az üzenetek kezeléséhez.

A kiszolgáló nélküli architektúra használatának van néhány működést érintő előnye:

  • Nincs szükség a kiszolgálók kezelésére.
  • A számítási erőforrások lefoglalása dinamikusan, igény szerint történik.
  • Kizárólag a kód végrehajtásához felhasznált számítási erőforrások után kell fizetni.
  • A számítási erőforrások igény szerint skálázhatók a forgalomtól függően.

Architektúra

A következő ábrán az alkalmazás magas szintű architektúrája látható:

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

Egy adatfolyamban a nyilak az eszközökről az Event Hubsra érkező üzeneteket jelenítik meg, és aktiválják a függvényalkalmazást. Az alkalmazásból az egyik nyíl egy tárolási üzenetsorba, egy másik pedig az Azure Cosmos DB-be való írást mutatja. Egy másik adatfolyamban a nyilak azt mutatják, hogy az ügyfél webalkalmazás statikus fájlokat kap a Blob Storage statikus webszolgáltatásától egy CDN-en keresztül. Egy másik nyíl az ÜGYFÉL HTTP-kérését jeleníti meg az API Managementen keresztül. Az API Managementben egy nyíl mutatja az Azure Cosmos DB-ből származó adatokat aktiváló és olvasó függvényalkalmazást. Egy másik nyíl a Microsoft Entra-azonosítón keresztüli hitelesítést mutatja. Egy felhasználó is bejelentkezik a Microsoft Entra-azonosítóba.

Eseménybetöltés:

  1. Az Azure Event Hubs feldolgozza a drónok üzeneteit.
  2. Az Event Hubs olyan eseménystreameket állít elő, amelyek tartalmazzák az üzenetadatokat.
  3. Ezek az események aktiválnak egy Azure Functions-alkalmazást a feldolgozásukhoz.
  4. Az eredmények tárolása az Azure Cosmos DB-ben történik.

Webalkalmazás:

  1. A statikus fájlokat a Blob Storage CDN-je üzemelteti.
  2. A felhasználó a Microsoft Entra ID használatával jelentkezik be a webalkalmazásba.
  3. Az Azure API Management átjáróként szolgál, amely egy REST API-végpontot tesz elérhetővé.
  4. Az ügyfél HTTP-kérései aktiválnak egy Azure Functions-alkalmazást, amely az Azure Cosmos DB-ből olvas, és visszaadja az eredményt.

Ez az alkalmazás két referenciaarchitektúrán alapszik, a fent leírt két funkcionális blokknak megfelelően:

Ezekből a cikkekből többet tudhat meg a magas szintű architektúráról, a megoldásban használt Azure-szolgáltatásokról és a méretezhetőségre, biztonságra és megbízhatóságra vonatkozó megfontolandó szempontokról.

Dróntelemetria függvény

Elsőként nézzük meg a függvényt, amely az Event Hubstól érkező drónüzeneteket dolgozza fel. A függvényt egy RawTelemetryFunction nevű osztály definiálja:

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

Ez az osztály több függőséggel rendelkezik, amelyeket a rendszer függőséginjektálással ad át a konstruktornak:

  • Az ITelemetryProcessor és IStateChangeProcessor interfészek két segédobjektumot határoznak meg. Mint rövidesen látni fogjuk, ezek az objektumok a legtöbb feladatot maguktól elvégzik.

  • A TelemetryClient az Application Insights SDK része. A használatával egyéni alkalmazásmetrikákat lehet küldeni az Application Insightsnak.

Később megnézzük, hogyan konfigurálhatja a függőséginjektálást. Egyelőre csak feltételezzük, hogy ezek a függőségek léteznek.

Event Hubs-trigger konfigurálása

A függvény logikáját egy RunAsync nevű aszinkron metódusként implementáljuk. Íme a metódus aláírása:

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

A metódus a következő paramétereket veszi fel:

  • A messages az eseményközpont-üzenetek tömbje.
  • A deadLetterMessages egy Azure Storage-üzenetsor, amely a kézbesíthetetlen üzenetek tárolására szolgál.
  • A logging egy naplózási felületet kínál az alkalmazásnaplók megírásához. Ezeket a naplókat a rendszer elküldi az Azure Monitornak.

A messages paraméter EventHubTrigger attribútuma konfigurálja az eseményindítót. Az attribútum tulajdonságai meghatározzák az eseményközpont nevét, a kapcsolati sztringet és a fogyasztói csoportot. (A fogyasztói csoport az Event Hubs eseménystreamjének izolált nézete. Ez az absztrakció lehetővé teszi ugyanazon eseményközpont több felhasználójának is.)

Látható, hogy egyes attribútumtulajdonságok százalékjeleket (%) tartalmaznak. Ezek azt jelzik, hogy a tulajdonság egy alkalmazásbeállítás nevét határozza meg, és a rendszer az adott alkalmazásbeállításból kéri le az értéket futásidőben. A százalékjelek nélküli tulajdonságok konstansértékeket adnak meg.

A Connection tulajdonság kivétel ez alól. Ez a tulajdonság mindig egy alkalmazásbeállítás nevét határozza meg, sosem a szövegkonstansértéket, így a százalékjel használata nem szükséges. Ennek a különbségnek az az oka, hogy a kapcsolati sztring titkos, és sosem kerül be a forráskódba.

Míg a másik két tulajdonság (az eseményközpont neve és a fogyasztói csoport) a kapcsolati sztringgel ellentétben nem bizalmas adat, ajánlott őket rögzített megadás helyett alkalmazásbeállításokban megadni. Ily módon az alkalmazás újrafordítása nélkül frissíthetők.

További információ az eseményindító konfigurálásáról: Azure Event Hubs-kötések az Azure Functionshöz.

Üzenetfeldolgozási logika

Az üzenetkötegeket feldolgozó RawTelemetryFunction.RunAsync metódus a következőképpen implementálható:

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

A függvény hívásakor a messages paraméter az eseményközpontból származó üzenetek tömbjét tartalmazza. Az üzenetek kötegenkénti feldolgozása jobb teljesítményt eredményez, mint az üzenetek egyesével történő elolvasása. Azonban meg kell győződnie arról, hogy a függvény rugalmas, és könnyedén kezeli a hibákat és a kivételeket. Ha a függvény nem kezelt kivételt dob fel kötegelés közben, elveszhet a többi üzenet. A téma további részletei a Hibakezelés szakaszban találhatók.

Ha figyelmen kívül hagyja a kivételkezelést, a szabályfeldolgozási logika minden üzenet esetén egyszerű:

  1. Hívja meg az ITelemetryProcessor.Deserialize metódust az eszközállapot-változást tartalmazó üzenet deszerializálásához.
  2. Hívja meg az IStateChangeProcessor.UpdateState metódust az állapotváltozás feldolgozásához.

Vizsgáljuk meg ezt a két metódust részletesebben, kezdve a Deserialize metódussal.

Deserialize metódus

A TelemetryProcess.Deserialize metódus egy olyan bájttömböt vesz fel, amely tartalmazza az üzenetek hasznos adatait. Deszerializálja ezeket a hasznos adatokat, majd egy DeviceState objektumot ad vissza, amely a drón állapotát jelöli. Az állapot részleges frissítést jelezhet, amely esetben csak az utolsó ismert állapothoz mért eltérést tartalmazza. Ezért a metódusnak kezelnie kell a null mezőket a deszerializált hasznos adatokban.

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

Ez a metódus egy másik, ITelemetrySerializer<T> nevű segédfelületet használ a nyers üzenet deszerializálásához. Az eredményeket utána POCO-modellé alakítja át, amely egyszerűbben használható. Ez a kialakítás segít izolálni a szabályfeldolgozási logikát a szerializációs implementáció részleteitől. Az ITelemetrySerializer<T> interfész egy megosztott kódtárban van meghatározva, amelyet az eszközszimulátor is használ szimulált eszközesemények létrehozására, majd továbbítja őket az Event Hubsba.

using System;

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

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

UpdateState metódus

A StateChangeProcessor.UpdateState metódus alkalmazza az állapotváltozásokat. Az egyes drónok utolsó ismert állapota JSON-dokumentumként van tárolva az Azure Cosmos DB-ben. Mivel a drónok részleges frissítéseket küldenek, az alkalmazás nem tudja egyszerűen felülírni a dokumentumot frissítés közben. Ehelyett le kell kérnie az előző állapotot, egyesítenie kell a mezőket, és egy upsert (frissítés/beszúrás) műveletet kell végrehajtania.

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

Ez a kód a IDocumentClient felület használatával lekéri a dokumentumot az Azure Cosmos DB-ből. Ha a dokumentum létezik, a rendszer egyesíti az új állapotértékeket a meglévő dokumentummal. Ellenkező esetben egy új dokumentum jön létre. Mindkét esetet az UpsertDocumentAsync metódus kezeli.

Ez a kód optimalizálva van arra az esetre, ha a dokumentum már létezik és egyesíthető. Egy adott dróntól érkező első telemetriai üzenetnél a ReadDocumentAsync metódus kivételt ad vissza, mivel még nem található dokumentum az adott drónhoz. Az első üzenet után a dokumentum elérhető lesz.

Figyelje meg, hogy ez az osztály függőséginjektálással injektálja az IDocumentClient Azure Cosmos DB-t és egy IOptions<T> konfigurációs beállításokat. A függőséginjektálás beállítását később tekintjük át.

Megjegyzés:

Az Azure Functions támogatja az Azure Cosmos DB kimeneti kötését. Ez a kötés lehetővé teszi, hogy a függvényalkalmazás kód nélkül írjon dokumentumokat az Azure Cosmos DB-ben. A kimeneti kötés azonban a szükséges egyéni upsert logika miatt nem fog működni ebben a forgatókönyvben.

Hibakezelés

Ahogy korábban említettük, a RawTelemetryFunction függvényalkalmazás egy hurokban dolgoz fel egy üzenetköteget. Ez azt jelenti, hogy a függvénynek minden kivételt könnyedén kell kezelnie, és folytatnia kell a köteg hátralevő részének feldolgozását. Ellenkező esetben a rendszer elvetheti az üzeneteket.

Ha a rendszer kivételt észlelt egy üzenet feldolgozása során, a függvény a kézbesíthetetlen levelek sorába helyezi az üzenetet:

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

A kézbesíthetetlen levelek sora egy tárolási üzenetsorra mutató kimeneti kötéssel van meghatározva:

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

Itt a Queue attribútum a kimeneti kötést, a StorageAccount attribútum pedig a tárfiók kapcsolati sztringjét tartalmazó alkalmazásbeállítás nevét határozza meg.

Üzembe helyezési tipp: A tárfiókot létrehozó Resource Manager-sablonban automatikusan feltölthet egy alkalmazásbeállítást a kapcsolati sztring. A trükk a listKeys függvény használata.

Itt látható az a sablonszakasz, amely létrehozza a tárfiókot a sorhoz:

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

Ez pedig a függvényalkalmazást létrehozó sablonszakasz.


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

Ez egy DeadLetterStorage nevű alkalmazásbeállítást határoz meg, amelynek értékét a listKeys függvénnyel lehet megadni. Fontos, hogy a függvényalkalmazás erőforrása a tárfiók erőforrásától függjön (lásd a dependsOn elemet). Ez garantálja, hogy először a tárfiók jöjjön létre, és a kapcsolati sztring elérhető legyen.

Függőséginjektálás beállítása

A következő kód beállítja a RawTelemetryFunction függvény függőséginjektálását:

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

A .NET-hez írt Azure Functions-függvények használhatják az ASP.NET Core függőséginjektálási keretrendszert. Az alapvető elképzelés az, hogy a felhasználó meghatározza a szerelvény indítási metódusát. A metódus egy IFunctionsHostBuilder interfészt használ, amellyel a függőséginjektálás függőségei határozhatók meg. Ehhez be kell hívni az Add* metódust a Services objektumon. Függőség hozzáadásakor meg kell határozni annak élettartamát:

  • Átmeneti objektumok akkor jönnek létre, amikor ezek létrehozását kéri.
  • Hatókörrel rendelkező objektumok függvényvégrehajtásonként egyszer jönnek létre.
  • Az egyedülálló objektumokat a rendszer újra felhasználja a függvényvégrehajtásokban a függvénygazdagép élettartamán belül.

Ebben a példában a TelemetryProcessor és StateChangeProcessor objektumok átmenetiként vannak meghatározva. Ez megfelelő megoldás az egyszerű, állapotmentes szolgáltatásokhoz. A DocumentClient osztálynak azonban a legjobb teljesítmény érdekében egyedülállónak kell lennie. További információkért lásd az Azure Cosmos DB és a .NET teljesítményével kapcsolatos tippeket.

Ha megtekinti a RawTelemetryFunction kódját, láthatja, hogy van egy másik függőség, amely nem jelenik meg a függőséginjektálás beállítási kódjában, a TelemetryClient osztály, amely az alkalmazásmetrikák naplózására szolgál. A Functions futtatókörnyezete automatikusan regisztrálja ezt az osztályt a függőséginjektálás tárolójában, hogy ne kelljen külön regisztrálnia.

Az Azure Functions függőséginjektálásával kapcsolatos további tudnivalókért tekintse át az alábbi cikkeket:

Konfigurációs beállítások átadása függőséginjektálásba

Időnként konfigurációs értékekkel kell inicializálni egy objektumot. Általában ezek a beállítások alkalmazásbeállításokból vagy (titkos kulcsok esetén) az Azure Key Vaultból származnak.

Ebben a témakörben két példát ismertetünk. Először az DocumentClient osztály egy Azure Cosmos DB-szolgáltatásvégpontot és -kulcsot vesz igénybe. Ehhez az objektumhoz az alkalmazás egy lambdát regisztrál, amelyet a függőséginjektálási tároló hív meg. A lambda az IConfiguration interfészt használja a konfigurációs értékek beolvasásához:

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

A második példa a StateChangeProcessor osztály. Ehhez az objektumhoz a beállításminta nevű megközelítést használjuk. Így működik:

  1. Határozza meg a konfigurációs beállításokat tartalmazó T osztályt. Ebben az esetben az Azure Cosmos DB-adatbázis neve és a gyűjtemény neve.

    public class StateChangeProcessorOptions
    {
        public string COSMOSDB_DATABASE_NAME { get; set; }
        public string COSMOSDB_DATABASE_COL { get; set; }
    }
    
  2. Adja meg a T osztályt a függőséginjektálás beállításosztályaként.

    builder.Services.AddOptions<StateChangeProcessorOptions>()
        .Configure<IConfiguration>((configSection, configuration) =>
        {
            configuration.Bind(configSection);
        });
    
  3. A konfigurálás alatt álló osztály konstruktorában adjon meg egy IOptions<T> paramétert.

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

A függőséginjektálási rendszer automatikusan kitölti a beállításosztályt a konfigurációs értékekkel, és átadja azt a konstruktornak.

Ennek a módszernek számos előnye van:

  • Szétválasztja az osztályt és a konfigurációs értékek forrását.
  • Könnyedén beállíthatók különböző konfigurációforrások, például környezeti változók vagy JSON konfigurációs fájlok.
  • Leegyszerűsíti az egységtesztelést.
  • Szigorú típusmeghatározású beállításosztályt használ, amely kevesebb hibalehetőséget rejt magában, mintha csak skaláris értékeket adna át.

A GetStatus függvény

Ebben a megoldásban a másik függvényalkalmazás egy egyszerű REST API-t implementál egy drón utolsó ismert állapotának lekéréséhez. Ez a függvény egy GetStatusFunction nevű osztályban van meghatározva. A függvény teljes kódja:

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

Ez a függvény HTTP-triggert használ egy HTTP GET kérés feldolgozásához, A függvény egy Azure Cosmos DB bemeneti kötéssel kéri le a kért dokumentumot. Lényeges, hogy a kötés azelőtt fusson, hogy a rendszer végrehajtaná a függvényben az engedélyezési logikát. A függvénykötés akkor is beolvassa a dokumentumot, ha egy jogosulatlan felhasználó kéri ki. Ezután az engedélyezési kód 401-es hibaüzenetet ad vissza, így a felhasználó nem fogja látni a dokumentumot. Az adott követelményektől függ, hogy ez a viselkedés elfogadható-e. Ez a megközelítés megnehezítheti például a bizalmas adatok hozzáférésének naplózását.

Hitelesítés és engedélyezés

A webalkalmazás Microsoft Entra-azonosítót használ a felhasználók hitelesítéséhez. Mivel az alkalmazás egy böngészőben futó egyoldalas alkalmazás (single-page application, SPA), az implicit engedélyezési folyamat megfelelő megoldást jelent:

  1. A webalkalmazás átirányítja a felhasználót az identitásszolgáltatóhoz (ebben az esetben a Microsoft Entra-azonosítóhoz).
  2. A felhasználó megadja a hitelesítő adatait.
  3. Az identitásszolgáltató visszairányítja a webalkalmazáshoz egy hozzáférési jogkivonattal.
  4. A webalkalmazás egy kérést küld a webes API felé, és a hozzáférési jogkivonatot az engedélyezési fejlécbe foglalja.

Implicit flow diagram

A függvényalkalmazások úgy is konfigurálhatók, hogy kód nélkül hitelesítsék a felhasználókat. További információkért lásd: Hitelesítés és engedélyezés az Azure App Service-ben.

Az engedélyezéshez azonban általában üzleti logikára van szükség. A Microsoft Entra ID támogatja a jogcímalapú hitelesítést. Ebben a modellben a felhasználó identitását az identitásszolgáltatótól származó jogcímek készlete képviseli. Jogcím lehet a felhasználóval kapcsolatos bármely információ, például a neve vagy az e-mail-címe.

A hozzáférési jogkivonat a felhasználói jogcímek alkészletét tartalmazza. Ezek között szerepelnek azok az alkalmazás-szerepkörök is, amelyekhez a felhasználó hozzá van rendelve.

A függvény principal paramétere egy ClaimsPrincipal objektum, amely tartalmazza a hozzáférési jogkivonat jogcímeit. Mindegyik jogcím a jogcímtípus és a jogcímérték kulcs/érték párja. Az alkalmazás ezekkel engedélyezi a kérést.

A következő kiterjesztésmetódus teszteli, hogy egy ClaimsPrincipal objektum tartalmaz-e szerepköröket. A false értéket adja vissza, ha valamelyik megadott szerepkör hiányzik. Ha hamis (false) értéket ad vissza, akkor a függvény HTTP 401 (Jogosulatlan) hibaüzenetet ad ki.

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

Az ebben az alkalmazásban való hitelesítésre és engedélyezésre vonatkozó további információért tekintse meg a referenciaarchitektúra biztonsági szempontokkal foglalkozó szakaszát.

Következő lépések

Miután megismerkedett a referenciamegoldás működésével, megismerheti a hasonló megoldásokra vonatkozó ajánlott eljárásokat és javaslatokat.

Az Azure Functions csak egy Azure számítási lehetőség. A számítási technológia kiválasztásával kapcsolatos segítségért lásd: Azure számítási szolgáltatás kiválasztása az alkalmazáshoz.