Code walkthrough: Serverloze toepassing met Functions

Serverloze modellen abstraheeren code uit de onderliggende rekeninfrastructuur, zodat ontwikkelaars zich kunnen richten op bedrijfslogica zonder uitgebreide installatie. Serverloze code vermindert de kosten, omdat u alleen betaalt voor de resources en duur van de code-uitvoering.

Het serverloze gebeurtenisgestuurde model past bij situaties waarin een bepaalde gebeurtenis een gedefinieerde actie activeert. Het ontvangen van een binnenkomend apparaatbericht activeert bijvoorbeeld opslag voor later gebruik, of een database-update activeert enige verdere verwerking.

Om u te helpen serverloze Azure-technologieën in Azure te verkennen, heeft Microsoft een serverloze toepassing ontwikkeld en getest die gebruikmaakt van Azure Functions. In dit artikel wordt de code voor de serverloze Functions-oplossing beschreven en worden ontwerpbeslissingen, implementatiedetails en enkele van de 'gotchas' beschreven die u kunt tegenkomen.

De oplossing verkennen

In de tweedelige oplossing wordt een hypothetisch systeem voor dronelevering beschreven. Drones versturen de vluchtstatus naar de cloud, waar deze berichten worden bewaard voor later gebruik. Met een web-app kunnen gebruikers de berichten ophalen om de meest recente status van de apparaten op te halen.

U kunt de code voor deze oplossing downloaden van GitHub.

In dit scenario wordt ervan uitgenomen dat u basiskennis hebt van de volgende technologieën:

U hoeft geen expert te zijn in Functions of Event Hubs, maar u moet de functies ervan wel grondig begrijpen. Dit zijn informatieve bronnen om mee te beginnen:

Inzicht in het scenario

Diagram van de functionele blokken

Fabrikam beheert een hele vloot drones voor een bezorgingsservice met drones. De toepassing bestaat uit twee functionele hoofdgebieden:

  • Gegevensopname. Tijdens de vlucht verzenden drones statusberichten naar een cloudeindpunt. De toepassing neemt de berichten op, verwerkt deze en schrijft de resultaten naar een database in de back-end (Cosmos DB). De apparaten versturen berichten in de indeling protocol buffer (protobuf). Protobuf is een efficiënte, zelfbeschrijvende serialisatie-indeling.

    Deze berichten bevatten gedeeltelijke updates. Met vaste intervallen verstuurt elke drone een 'sleutelframebericht' dat alle statusvelden bevat. Tussen de sleutelframes bevatten statusberichten alleen velden die sinds het laatste bericht zijn gewijzigd. De gedrag is normaal voor veel IoT-apparaten die bandbreedte en energie moeten besparen.

  • Web-app. Met een webtoepassing kunnen gebruikers een apparaat opzoeken en de laatst bekende status van het apparaat opvragen. Gebruikers moeten zich aanmelden bij de toepassing en verifiëren met Azure Active Directory (Azure AD). De toepassing staat alleen aanvragen toe van gebruikers die zijn gemachtigd voor toegang tot de app.

Dit is een schermopname van de webtoepassing met het resultaat van een query:

Schermopname van de client-app

De toepassing ontwerpen

Fabrikam heeft besloten om Azure Functions te gebruiken om de bedrijfslogica van de toepassing te implementeren. Azure Functions is een voorbeeld van 'Function-as-a-Service' (Faas). In dit rekenmodel is een functie een stukje code dat wordt geïmplementeerd in de cloud en wordt uitgevoerd in een hostingomgeving. Deze hostingomgeving isoleert de servers waarop de code wordt uitgevoerd volledig.

Waarom kiezen voor een serverloze benadering?

Een serverloze architectuur met Functions is een voorbeeld van een gebeurtenisafhankelijke architectuur. De functiecode wordt geactiveerd door een gebeurtenis buiten de functie —, in dit geval een bericht van een drone of een HTTP-aanvraag van een clienttoepassing. Met een functie-app hoeft u geen code te schrijven voor de trigger. U schrijft alleen de code die wordt uitgevoerd in reactie op de trigger. Dit betekent dat u zich kunt richten op uw bedrijfslogica, in plaats van dat u veel code moet schrijven om zaken in de infrastructuur, zoals berichten, aan de gang te krijgen.

Er zijn ook operationele voordelen voor het gebruik van een serverloze architectuur:

  • Het is niet meer nodig om servers te beheren.
  • Rekenresources worden naar behoefte dynamisch toegewezen.
  • Er worden alleen kosten in rekening gebracht voor de rekenresources die zijn gebruikt om uw code uit te voeren.
  • De rekenresources worden op aanvraag geschaald op basis van het verkeer.

Architectuur

In het volgende diagram wordt de architectuur van de toepassing op hoog niveau weergegeven:

Diagram met de architectuur op hoog niveau van de serverloze Functions-toepassing.

In één gegevensstroom geven pijlen berichten weer die van Apparaten naar Event Hubs en de functie-app activeren. In de app ziet u een pijl met berichten van in wachtrij geplaatste berichten die in een wachtrij staan en een andere pijl die het schrijven naar Azure Cosmos DB. In een andere gegevensstroom tonen pijlen de clientweb-app statische bestanden van statische webhosting van Blob Storage via een CDN. Een andere pijl toont de HTTP-aanvraag van de client die via de API Management. In API Management pijl ziet u dat de functie-app gegevens activeert en leest uit Azure Cosmos DB. Een andere pijl toont verificatie via Azure AD. Een gebruiker meldt zich ook aan bij Azure AD.

Gegevensopname:

  1. Droneberichten worden door Azure Event Hubs opgenomen.
  2. Event Hubs produceert een stroom gebeurtenissen die de berichtgegevens bevatten.
  3. Deze gebeurtenissen activeren een Azure Functions-app om de gegevens te verwerken.
  4. De resultaten worden bewaard in Cosmos DB.

Web-app:

  1. Er worden door CDN statische bestanden aangeboden vanuit blob-opslag.
  2. Een gebruiker meldt zich aan bij de web-app met Azure AD.
  3. Azure API Management fungeert als gateway die een REST API-eindpunt beschikbaar maakt.
  4. HTTP-aanvragen van de client activeren een Azure Functions-app die uit Cosmos DB leest en het resultaat retourneert.

Deze toepassing is gebaseerd op twee referentiearchitecturen die corresponderen met de twee functionele blokken die hierboven zijn beschreven:

Lees deze artikelen voor meer informatie over de architectuur op hoog niveau, de Azure-services die in de oplossing worden gebruikt, en overwegingen over schaalbaarheid, beveiliging en betrouwbaarheid.

Telemetriefunctie van de drone

We kijken allereerst naar de functie die droneberichten uit Event Hubs verwerkt. De functie wordt gedefinieerd in een klasse met de naam 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;
        }
    }
    ...
}

Deze klasse heeft verschillende afhankelijkheden die in de constructor worden geïnjecteerd door middel van afhankelijkheidsinjectie:

  • De interfaces ITelemetryProcessor en IStateChangeProcessor definiëren twee helperobjecten. De objecten voeren het grootste deel van het werk uit.

  • De TelemetryClient is onderdeel van de Application Insights SDK en wordt gebruikt om aangepaste metrische toepassingsgegevens te sturen naar Application Insights.

U bekijkt later hoe u de afhankelijkheidsinjectie configureert. Neem voor nu aan dat deze afhankelijkheden bestaan.

De Event Hubs-trigger configureren

De logica in deze functie wordt geïmplementeerd als asynchrone methode met de naam RunAsync. Hier volgt de handtekeningmethode:

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

De methode maakt gebruik van de volgende parameters:

  • messages is een matrix van Event Hub-berichten.
  • deadLetterMessages is een Azure Storage-wachtrij die wordt gebruikt om onbestelbare berichten op te slaan.
  • logging biedt een logboekinterface om toepassingslogboeken te schrijven. Deze logboeken worden naar Azure Monitor verstuurd.

Het kenmerk EventHubTrigger in de parameter messages configureert de trigger. De eigenschappen van het kenmerk specificeren een Event Hub-naam, een verbindingsreeks en een consumentengroep. (Een consumentengroep is een geïsoleerde weergave van de Event Hubs-gebeurtenisstroom. Door deze abstractie zijn meerdere consumenten voor dezelfde Event Hub mogelijk.)

Let op de procenttekens (%) in een aantal van de kenmerkeigenschappen. Deze geven aan dat de eigenschap de naam van een app-instelling definieert. De werkelijke waarde wordt tijdens de runtime van die instelling overgenomen. Zonder procenttekens zou de eigenschap de letterlijke waarde tonen.

De eigenschap Connection vormt een uitzondering. Deze eigenschap definieert altijd de naam van een app-instelling en toont nooit de letterlijke waarde. Daarom is een procentteken niet nodig. De reden voor dit verschil is dat een verbindingsreeks geheim is en nooit moet worden vermeld in de broncode.

Hoewel de andere twee eigenschappen (Event Hub-naam en consumentengroep) geen gevoelige gegevens zijn, zoals een verbindingsreeks dat wel is, is het nog steeds beter om deze in de app-instellingen te plaatsen in plaats van in de code op te nemen. Op die manier kunnen ze worden bijgewerkt zonder de app opnieuw te compileren.

Raadpleeg Azure Event Hubs-bindingen voor Azure Functions voor meer informatie over de configuratie van deze trigger.

Logica voor de verwerking van berichten

Hier volgt de implementatie van de methode RawTelemetryFunction.RunAsync waarmee een batch berichten wordt verwerkt:

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

Wanneer de functie wordt aangeroepen, bevat de parameter messages een matrix berichten uit de Event Hub. De verwerking van berichten in batches zorgt in het algemeen voor betere prestaties dan wanneer alle berichten één voor één worden gelezen. U moet er echter voor zorgen dat de functie tolerant is, en fouten en uitzonderingen correct verwerkt. Als dit niet zo is en de functie een onverwerkte uitzondering in het midden van een batch verwerkt, verliest u de resterende berichten. Deze overweging wordt in meer detail besproken in de sectie Foutafhandeling.

Maar als u de uitzonderingsverwerking negeert, is de verwerkingslogica voor elk bericht eenvoudig:

  1. Roep ITelemetryProcessor.Deserialize aan om het bericht met de statuswijziging van een apparaat te deserialiseren.
  2. Roep IStateChangeProcessor.UpdateState aan om de statuswijziging te verwerken.

Bekijk deze twee methoden in meer detail en begin met de methode Deserialize.

De methode op basis van deserialiseren

De methode TelemetryProcess.Deserialize maakt gebruik van een bytematrix die de payload van de berichten bevat. Deze deserialiseert de payload en retourneert het object DeviceState, dat voor de status van een drone staat. De status staat mogelijk voor een gedeeltelijke update met alleen de delta van de laatst bekende status. Daarom moet de methode null-velden verwerken in de gedeserialiseerde payload.

public class TelemetryProcessor : ITelemetryProcessor
{
    private readonly ITelemetrySerializer<DroneState> serializer;

    public TelemetryProcessor(ITelemetrySerializer<DroneState> serializer)
    {
        this.serializer = serializer;
    }

    public DeviceState Deserialize(byte[] payload, ILogger log)
    {
        DroneState restored = serializer.Deserialize(payload);

        log.LogInformation("Deserialize message for device ID {DeviceId}", restored.DeviceId);

        var deviceState = new DeviceState();
        deviceState.DeviceId = restored.DeviceId;

        if (restored.Battery != null)
        {
            deviceState.Battery = restored.Battery;
        }
        if (restored.FlightMode != null)
        {
            deviceState.FlightMode = (int)restored.FlightMode;
        }
        if (restored.Position != null)
        {
            deviceState.Latitude = restored.Position.Value.Latitude;
            deviceState.Longitude = restored.Position.Value.Longitude;
            deviceState.Altitude = restored.Position.Value.Altitude;
        }
        if (restored.Health != null)
        {
            deviceState.AccelerometerOK = restored.Health.Value.AccelerometerOK;
            deviceState.GyrometerOK = restored.Health.Value.GyrometerOK;
            deviceState.MagnetometerOK = restored.Health.Value.MagnetometerOK;
        }
        return deviceState;
    }
}

Deze methode maakt gebruikt van een andere helperinterface, ITelemetrySerializer<T>, om het onbewerkte bericht te deserialiseren. De resultaten worden vervolgens getransformeerd in eenPOCO-model waarmee u makkelijker kunt werken. Dit ontwerp helpt de verwerkingslogica te isoleren van de implementatiedetails van de serialisatie. De interface ITelemetrySerializer<T> is gedefinieerd in een gedeelde bibliotheek, die ook wordt gebruikt door de apparaatsimulator om gesimuleerde apparaatgebeurtenissen te genereren en deze naar Event Hubs te sturen.

using System;

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

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

De methode UpdateState

De methode StateChangeProcessor.UpdateState past de statuswijzigingen toe. De laatst bekende status voor elke drone wordt als JSON-document bewaard in Cosmos DB. Omdat de drones gedeeltelijke updates versturen, kan de toepassing het document niet simpelweg overschrijven wanneer er een update wordt ontvangen. In plaats daarvan moet de toepassing de vorige status ophalen, de velden samenvoegen en vervolgens een upsert-bewerking uitvoeren.

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

Deze code gebruikt de interface IDocumentClient om een document uit Cosmos DB op te halen. Als het document bestaat, worden de nieuwe statuswaarden samengevoegd in het bestaande document. Anders wordt er een nieuw document gemaakt. Beide gevallen worden verwerkt door de methode UpsertDocumentAsync.

Deze code is geoptimaliseerd voor wanneer het document al bestaat en kan worden samengevoegd. In het eerste telemetriebericht van een bepaalde drone levert de methode ReadDocumentAsync een uitzondering op, omdat er geen document is voor die drone. Na het eerste bericht is het document beschikbaar.

U ziet dat deze klasse gebruikmaakt van afhankelijkheidsinjectie om de IDocumentClient te injecteren voor Cosmos DB en van een IOptions<T> met configuratie-instellingen. U ziet later hoe u afhankelijkheidsinjectie instelt.

Notitie

Azure Functions ondersteunt een uitvoerbinding voor Cosmos DB. Met deze binding kan de functie-app zonder code documenten schrijven in Cosmos DB. De uitvoerbinding werkt echter niet voor dit specifieke scenario, omdat de aangepaste upsert-logica nodig is.

Foutafhandeling

Zoals eerder aangegeven, verwerkt de functie-app RawTelemetryFunction een batch berichten in een lus. Dat betekent dat de functie uitzonderingen correct moet verwerken en door moet gaan met de verwerking van de rest van de batch. Anders worden er mogelijk berichten verwijderd.

Als er een uitzondering is opgetreden wanneer een bericht wordt verwerkt, plaatst de functie het bericht in een wachtrij met onbestelbare berichten:

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

Die wachtrij wordt met een uitvoerbinding gedefinieerd voor een opslagwachtrij:

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

Hier specificeert het kenmerk Queue de uitvoerbinding en het kenmerk StorageAccount de naam van een app-instelling die de verbindingsreeks bevat voor het opslagaccount.

Implementatietip: In de Resource Manager waarmee het opslagaccount wordt gemaakt, kunt u automatisch een app-instelling vullen met de connection string. De trick is om de functie listKeys te gebruiken.

Hier volgt de sectie van de sjabloon die het opslagaccount voor de wachtrij maakt:

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

Dit is de sectie van de sjabloon die de functie-app maakt.


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

Deze definieert een app-instelling met de naam DeadLetterStorage, waarvan de waarde wordt ingevuld met de functie listKeys. Het is belangrijk om de resource van de functie-app afhankelijk te maken van de resource van het opslagaccount (bekijk het element dependsOn). Dit zorgt ervoor dat het opslagaccount eerst wordt gemaakt en dat de verbindingsreeks beschikbaar is.

Afhankelijkheidsinjectie instellen

Met de volgende code stelt u afhankelijkheidsinjectie in voor de functie 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 Functions voor .NET kan gebruikmaken van het framework van ASP.NET Core-afhankelijkheidsinjectie. Het uitgangspunt is dat u een opstartmethode voor uw assembly declareert. De methode maakt gebruik van de interface IFunctionsHostBuilder, die wordt gebruikt om de afhankelijkheden voor afhankelijkheidsinjectie te declareren. U doet dit door de methode Add* aan te roepen in het object Services. Wanneer u een afhankelijkheid toevoegt, geeft u de levensduur ervan op:

  • Tijdelijke objecten worden elke keer dat deze nodig zijn gemaakt.
  • Objecten in scope worden een keer per uitvoering van een functie gemaakt.
  • Singleton-objecten worden binnen de levensduur van de functiehost voor de uitvoering van functies opnieuw gebruikt.

In dit voorbeelden zijn de objecten TelemetryProcessor en StateChangeProcessor opgegeven als tijdelijk. Dit is geschikt voor lichte, stateless services. Aan de andere kant moet de klasse DocumentClient een singleton zijn voor de beste prestaties. Raadpleeg Tips voor betere prestaties van Azure Cosmos DB en .NET voor meer informatie.

Als u de code voor RawTelemetryFunction opnieuw raadpleegt, ziet u daar een afhankelijkheid die niet in de instellingscode voor afhankelijkheidsinjectie verschijnt, namelijk de klasse TelemetryClient die wordt gebruikt om metrische toepassingsgegevens te registreren. De Functions-runtime registreert deze klasse automatisch in de afhankelijkheidsinjectiecontainer, zodat u deze niet expliciet hoeft te registreren.

Voor meer informatie over afhankelijkheidsinjectie in Azure Functions raadpleegt u de volgende artikelen:

Configuratie-instellingen doorgeven in afhankelijkheidsinjectie

Soms moet een object worden geïnitialiseerd met een aantal configuratiewaarden. In het algemeen komen deze instellingen uit app-instellingen of (in het geval van geheimen) uit Azure Key Vault.

Er zijn twee voorbeelden in deze toepassing. Allereerst maakt de klasse DocumentClient gebruik van een Cosmos DB-service-eindpunt en van een sleutel. Voor dit object registreert de toepassing een lambda die door de afhankelijkheidsinjectiecontainer wordt aangeroepen. Deze lambda maakt gebruik van de interface IConfiguration om de configuratiewaarden te lezen:

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

Het tweede voorbeeld is de klasse StateChangeProcessor. Voor dit object gebruiken we een benadering die het optiepatroon wordt genoemd. Het werkt als volgt:

  1. Definieer een klasse T die uw configuratie-instellingen bevat. In dit geval betreft het de naam van de Cosmos DB-database en -verzameling.

    public class StateChangeProcessorOptions
    {
        public string COSMOSDB_DATABASE_NAME { get; set; }
        public string COSMOSDB_DATABASE_COL { get; set; }
    }
    
  2. Voeg de klasse T toe als een optieklasse voor afhankelijkheidsinjectie.

    builder.Services.AddOptions<StateChangeProcessorOptions>()
        .Configure<IConfiguration>((configSection, configuration) =>
        {
            configuration.Bind(configSection);
        });
    
  3. Neem in de constructor van de klasse die wordt geconfigureerd, een parameter IOptions<T> op.

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

Het afhankelijkheidsinjectiesysteem vult automatisch de optieklasse in met de configuratiewaarden en geeft deze door aan de constructor.

Deze benadering kent een aantal voordelen:

  • De klasse wordt van de bron van de configuratiewaarden losgekoppeld.
  • Configuratiebronnen, bijvoorbeeld omgevingsvariabelen of JSON-configuratiebestanden, kunnen eenvoudig worden ingesteld.
  • Moduletests worden eenvoudiger.
  • Er wordt gebruikgemaakt van een sterk getypeerde optieklasse die minder foutgevoelig is dan wanneer er alleen scalaire waarden worden doorgegeven.

De functie GetStatus

De andere Functions-app in deze oplossing implementeert een eenvoudige REST API om de laatst bekende status van een drone op te halen. Deze functie is gedefinieerd in een klasse met de naam GetStatusFunction. Dit is de volledige code voor de functie:

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

Deze functie maakt gebruik van een HTTP-trigger om een HTTP GET-aanvraag te verwerken. De functie gebruikt een Cosmos DB-invoerbinding om het aangevraagde document op te halen. Een overweging is dat deze binding wordt uitgevoerd voordat de autorisatielogica binnen de functie is uitgevoerd. Als een onbevoegde gebruiker een document aanvraagt, haalt de functiebinding toch het document op. Vervolgens retourneert de autorisatiecode een 401-fout, zodat de gebruiker het document niet ziet. Of dit gedrag aanvaardbaar is, is afhankelijk van uw vereisten. Deze benadering maakt het bijvoorbeeld moeilijker om gegevenstoegang tot gevoelige gegevens te controleren.

Verificatie en autorisatie

De web-app maakt gebruik van Azure AD om gebruikers te verifiëren. Omdat de app een toepassing met één pagina is die wordt uitgevoerd in de browser, is de impliciete toekenningsstroom geschikt:

  1. De web-app stuurt de gebruiker door naar de id-provider (in dit geval Azure AD).
  2. De gebruiker voert zijn referenties in.
  3. De id-provider stuurt de gebruiker terug naar de web-app met een toegangstoken.
  4. De web-app stuurt een aanvraag naar de web-API en neemt de toegangstoken op in de autorisatie-header.

Diagram van impliciete stroom

Een Function-toepassing kan worden geconfigureerd om gebruikers te verifiëren met een nulcode. Raadpleeg Verificatie en autorisatie in Azure App Service voor meer informatie.

Aan de andere kant is voor autorisatie doorgaans bedrijfslogica nodig. Azure AD ondersteunt op claims gebaseerde verificatie. In dit model is de identiteit van een gebruiker vertegenwoordigd als een set claims van de id-provider. Een claim kan elk soort informatie over de gebruiker zijn, bijvoorbeeld een naam of e-mailadres.

Het toegangstoken bevat een subset gebruikersclaims. Hieronder vallen ook de toepassingsrollen die de gebruiker toegewezen heeft gekregen.

De parameter principal van de functie is een ClaimsPrincipal-object dat de claims van het toegangstoken bevat. Elke claim is een sleutel-waardepaar van het claimtype en de claimwaarde. De toepassing gebruikt deze om de aanvraag te autoriseren.

De volgende extensiemethode test of een ClaimsPrincipal-object een set rollen bevat. Deze retourneert false als een van de opgegeven rollen ontbreekt. Als deze methode 'false' retourneert, retourneert de functie HTTP 401 (niet geautoriseerd).

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

Voor meer informatie over verificatie en autorisatie in deze toepassing raadpleegt u de sectie Beveiligingsoverwegingen van de referentiearchitectuur.

Volgende stappen

Zodra u weet hoe deze referentieoplossing werkt, leert u best practices en aanbevelingen voor vergelijkbare oplossingen.

Azure Functions is slechts één Azure-rekenoptie. Zie Een Azure-rekenservice kiezen voor uw toepassing voor hulp bij het kiezen van een rekentechnologie.