Overzicht van code: Serverloze toepassing met Functions

Azure Event Hubs
Azure Functions

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

Het serverloze gebeurtenisgestuurde model past in situaties waarin een bepaalde gebeurtenis een gedefinieerde actie activeert. Als u bijvoorbeeld een binnenkomend apparaatbericht ontvangt, wordt de opslag geactiveerd voor later gebruik, of een database-update activeert een verdere verwerking.

Om u te helpen met het verkennen van serverloze Azure-technologieën in Azure, 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

De tweedelige oplossing beschrijft een hypothetisch droneleveringssysteem. 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 via GitHub.

In dit scenario wordt uitgegaan van basiskennis 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 of the functional blocks

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 deze berichten op en verwerkt deze berichten en schrijft de resultaten naar een back-enddatabase (Azure 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 zich verifiëren met Microsoft Entra-id. 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:

Screenshot of 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 computingmodel is een functie een stukje code dat in de cloud wordt geïmplementeerd 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 is een geactiveerde gebeurtenis die zich buiten de functie bevindt, 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 showing the high-level architecture of the serverless Functions application.

In één gegevensstroom geven pijlen berichten weer van apparaten naar Event Hubs en activeren van de functie-app. In de app ziet u met één pijl berichten met dode letters naar een opslagwachtrij en een andere pijl die het schrijven naar Azure Cosmos DB weergeeft. In een andere gegevensstroom laten pijlen zien dat de clientweb-app statische bestanden ophaalt uit statische blobopslaghosting, via een CDN. Een andere pijl toont de HTTP-aanvraag van de client via API Management. Vanuit API Management toont één pijl de functie-app die gegevens activeert en leest vanuit Azure Cosmos DB. Een andere pijl toont verificatie via Microsoft Entra-id. Een gebruiker meldt zich ook aan bij Microsoft Entra ID.

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 opgeslagen in Azure 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 behulp van Microsoft Entra ID.
  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 wordt gelezen uit Azure Cosmos DB en retourneert het resultaat.

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. Met deze abstractie kunnen meerdere consumenten van dezelfde Event Hub.)

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 opgeslagen als een JSON-document in Azure 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 maakt gebruik van de IDocumentClient interface om een document op te halen uit Azure Cosmos DB. 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 azure IDocumentClient Cosmos DB en een IOptions<T> met configuratie-instellingen te injecteren. U ziet later hoe u afhankelijkheidsinjectie instelt.

Notitie

Azure Functions ondersteunt een uitvoerbinding voor Azure Cosmos DB. Met deze binding kan de functie-app documenten schrijven in Azure Cosmos DB zonder code. 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-sjabloon waarmee het opslagaccount wordt gemaakt, kunt u automatisch een app-instelling vullen met de verbindingsreeks. De truc 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. Eerst gebruikt de DocumentClient klasse een Azure Cosmos DB-service-eindpunt en -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. Dit werkt als volgt:

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

    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 maakt gebruik van een Azure 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 gebruikt Microsoft Entra-id 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 leidt de gebruiker om naar de id-provider (in dit geval Microsoft Entra-id).
  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.

Implicit flow diagram

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. Microsoft Entra ID ondersteunt verificatie op basis van claims. 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 voor uw toepassing kiezen voor hulp bij het kiezen van een rekentechnologie.