Modifier

Partager via


Présentation détaillée du code : Application serverless avec Fonctions

Hubs d'événements Azure
Azure Functions

Les modèles serverless extraient le code de l’infrastructure de calcul sous-jacente, ce qui permet aux développeurs de se concentrer sur la logique métier sans avoir à procéder à une configuration approfondie. Le code serverless réduit les coûts, car vous payez uniquement pour les ressources d’exécution du code et la durée.

Le modèle basé sur des événements serverless convient aux situations où un certain événement déclenche une action définie. Par exemple, la réception d’un message de l’appareil entrant déclenche le stockage en vue d’une utilisation ultérieure, ou une mise à jour de la base de données déclenche un traitement supplémentaire.

Pour vous aider à explorer les technologies serverless Azure dans Azure, Microsoft a développé et testé une application serverless qui utilise Azure Functions. Cet article vous guide dans le code de la solution Functions serverless et décrit les décisions de conception, les détails de mis en œuvre et certains des « pièges » que vous pouvez rencontrer.

Explorez la solution

Cette solution en deux parties décrit un hypothétique système de livraison par drone. Les drones envoient un état du vol en cours au cloud, lequel stocke ces messages en vue d’une utilisation ultérieure. Une application Web permet aux utilisateurs de récupérer les messages pour obtenir l’état le plus récent des appareils.

Vous pouvez télécharger le code pour cette solution à partir de GitHub.

Cette présentation détaillée part du principe que vous avez une connaissance de base des technologies suivantes :

Vous n’avez pas besoin d’être expert en ce qui concerne Azure Functions ou Event Hubs, mais vous devez comprendre leurs fonctionnalités à un niveau général. Voici quelques précieuses ressources pour bien démarrer :

Présentation du scénario

Diagramme des blocs fonctionnels

Fabrikam gère un parc de drones pour un service de livraison par drone. L’application se compose de deux zones fonctionnelles :

  • Ingestion d’événements. Pendant le vol, les drones envoient des messages d’état à un point de terminaison cloud. L’application ingère et traite ces messages, et écrit les résultats dans une base de données back-end (Azure Cosmos DB). Les appareils envoient des messages au format Protocol Buffers (protobuf). Protobuf est un format de sérialisation efficace et autodescriptif.

    Ces messages contiennent des mises à jour partielles. À intervalle fixe, chaque drone envoie un message « image clé » qui contient tous les champs d’état. Entre les images clés, les messages d’état incluent uniquement les champs qui ont changé depuis le dernier message. Ce comportement est typique de nombreux appareils IoT qui doivent économiser la bande passante et la puissance.

  • Application web. Une application web permet aux utilisateurs de rechercher un appareil et d’interroger son dernier état connu. Les utilisateurs doivent se connecter à l’application et s’authentifier auprès de Microsoft Entra ID. L’application autorise uniquement les requêtes des utilisateurs qui sont autorisés à accéder à l’application.

Voici une capture d’écran de l’application web montrant le résultat d’une requête :

Capture d’écran d’une application cliente

Concevoir l’application

Fabrikam a décidé d’utiliser Azure Functions pour implémenter la logique métier d’application. Azure Functions est un exemple de « Fonction en tant que Service » (FaaS). Dans ce modèle informatique, une fonction est un bloc de code déployé sur le cloud et exécuté dans un environnement d’hébergement. Cet environnement d’hébergement extrait les serveurs qui exécutent le code.

Pourquoi choisir une approche serverless ?

Une architecture serverless avec Azure Functions est un exemple d’architecture basée sur les événements. Le code de fonction est déclenché par un événement externe à la fonction, en l’occurrence un message provenant d’un drone ou une requête HTTP d’une application cliente. Avec une application de fonction, vous n’avez pas besoin d’écrire du code pour le déclencheur. Vous écrivez uniquement le code qui s’exécute en réponse au déclencheur. Cela signifie que vous pouvez vous concentrer sur votre logique métier, plutôt qu’écrire beaucoup de code pour gérer les aspects liés à l’infrastructure tels que la messagerie.

Le recours à une architecture serverless offre également des avantages opérationnels :

  • Vous n’avez pas besoin de gérer les serveurs.
  • Les ressources de calcul sont allouées de façon dynamique en fonction des besoins.
  • vous êtes facturé uniquement pour les ressources de calcul qui ont été utilisées pour exécuter votre code.
  • Les ressources de calcul sont mises à l’échelle à la demande en fonction du trafic.

Architecture

Le diagramme suivant illustre l’architecture générale de l’application :

Diagramme montrant l’architecture de haut niveau de l’application de fonction sans serveur.

Dans un même flux de données, les flèches affichent les messages qui passent des appareils à Event Hubs et déclenchent l’application de fonction. À partir de l’application, une flèche affiche les messages non distribués qui vont vers une file d’attente de stockage, et une autre flèche indique l’écriture dans Azure Cosmos DB. Dans un autre flux de données, les flèches montrent l’application web cliente obtenant des fichiers statiques à partir de l’hébergement web statique du stockage d’objets BLOB, via un CDN. Une autre flèche affiche la requête HTTP du client qui passe par la gestion des API. À partir de la gestion des API, une flèche montre l’application de fonction déclenchant et lisant des données à partir d’Azure Cosmos DB. Une autre flèche montre l’authentification via Microsoft Entra ID. Un utilisateur se connecte également à Microsoft Entra ID.

Ingestion d’événements :

  1. Les messages des drones sont reçus par Azure Event Hubs.
  2. Event Hubs produit un flux d’événements qui contient les données des messages.
  3. Ces événements déclenchent une application Azure Functions afin de les traiter.
  4. Les résultats sont stockés dans Azure Cosmos DB.

Application web :

  1. Les fichiers statiques sont pris en charge par le CDN à partir du stockage d’objets blob.
  2. Un utilisateur se connecte à l’application web à l’aide de Microsoft Entra ID.
  3. Gestion des API Azure agit en tant que passerelle qui expose un point de terminaison d’API REST.
  4. Les requêtes HTTP en provenance du client déclenchent une application Azure Functions qui lit à partir d’Azure Cosmos DB et retourne le résultat.

Cette application est basée sur deux architectures de référence correspondant aux deux blocs fonctionnels décrits ci-dessus :

Vous pouvez lire ces articles pour en savoir plus sur l’architecture générale, les services Azure utilisés dans la solution et les considérations en matière de scalabilité, d’évolutivité, de sécurité et de fiabilité.

Fonction de télémétrie de drone

Commençons par examiner la fonction qui traite les messages des drones à partir d’Event Hubs. La fonction est définie dans une classe nommée 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;
        }
    }
    ...
}

Cette classe a plusieurs dépendances, qui sont injectées dans le constructeur à l’aide de l’injection de dépendances :

  • Les interfaces ITelemetryProcessor et IStateChangeProcessor définissent deux objets d’assistance. Comme nous le verrons, ces objets effectuent la plupart du travail.

  • TelemetryClient fait partie du Kit de développement logiciel (SDK) Application Insights (API classique). Il est utilisé pour envoyer des métriques d’application personnalisées à Application Insights.

Plus tard, nous verrons comment configurer l’injection de dépendances. Pour l’instant, partons simplement du principe que ces dépendances existent.

Configurer le déclencheur Event Hubs

La logique dans la fonction est implémentée en tant que méthode asynchrone nommée RunAsync. Voici la signature de méthode :

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

La méthode prend les paramètres suivants :

  • messages est un tableau de messages de hub d’événements.
  • deadLetterMessages est une file d’attente Stockage Azure utilisée pour stocker les messages restés lettres mortes.
  • logging fournit une interface de journalisation pour l’écriture des journaux d’application. Ces journaux sont envoyés à Azure Monitor.

L’attribut EventHubTrigger du paramètre messages configure le déclencheur. Les propriétés de l’attribut spécifient un nom de hub d’événements, une chaîne de connexion et un groupe de consommateurs. (Un groupe de consommateurs est une vue isolée du flux d’événements Event Hubs. Cette abstraction autorise plusieurs consommateurs d’un même hub d’événements.)

Notez les signes de pourcentage (%) dans certaines des propriétés d’attributs. Ils indiquent que la propriété spécifie le nom d’un paramètre d’application, et que la valeur réelle est obtenue à partir de ce paramètre d’application au moment de l’exécution. Autrement, sans signe de pourcentage, la propriété donne la valeur littérale.

La propriété Connection est une exception. Elle spécifie toujours un nom de paramètre d’application, jamais une valeur littérale. Le symbole de pourcentage n’est donc pas nécessaire. La raison de cette distinction est qu’une chaîne de connexion est secrète et ne doit jamais être archivée dans le code source.

Même si les deux autres propriétés (nom du hub d’événements et groupe de consommateurs) ne sont pas des données sensibles comme une chaîne de connexion, il est tout de même préférable de les placer dans les paramètres de l’application plutôt que de les coder en dur. Ainsi, ils peuvent être mis à jour sans recompiler l’application.

Pour plus d’informations sur la configuration de ce déclencheur, consultez Liaisons Azure Event Hubs pour Azure Functions.

Logique de traitement des messages

Voici l’implémentation de la méthode RawTelemetryFunction.RunAsync qui traite un lot de messages :

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

Quand la fonction est appelée, le paramètre messages contient un tableau de messages provenant du hub d’événements. Le traitement par lot des messages est généralement plus performant que la lecture d’un message à la fois. Toutefois, vous devez vous assurer que la fonction est résiliente et gère correctement les échecs et exceptions. Sinon, si la fonction lève une exception non gérée au milieu d’un lot, vous risquez de perdre les messages restants. Cette considération est abordée plus en détail dans la section Gestion des erreurs.

Mais si vous ignorez la gestion des exceptions, la logique de traitement pour chaque message est simple :

  1. Appelez ITelemetryProcessor.Deserialize pour désérialiser le message qui contient un changement d’état d’appareil.
  2. Appelez IStateChangeProcessor.UpdateState pour traiter le changement d’état.

Examinons ces deux méthodes plus en détail, en commençant par Deserialize.

Méthode Deserialize

La méthode TelemetryProcess.Deserialize prend un tableau d’octets qui contient la charge utile de message. Elle désérialise cette charge utile et retourne un objet DeviceState qui représente l’état d’un drone. L’état peut représenter une mise à jour partielle, contenant uniquement le delta par rapport au dernier état connu. Par conséquent, la méthode doit gérer les champs null dans la charge utile désérialisée.

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

Cette méthode utilise une autre interface d’assistance, ITelemetrySerializer<T>, pour désérialiser le message brut. Les résultats sont ensuite transformés en un modèle POCO avec lequel il est plus facile de travailler. Cette conception aide à isoler la logique de traitement des détails d’implémentation de sérialisation. L’interface ITelemetrySerializer<T> est définie dans une bibliothèque partagée, qui est également utilisée par le simulateur d’appareil pour générer des événements d’appareil simulé et les envoyer à Event Hubs.

using System;

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

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

Méthode UpdateState

La méthode StateChangeProcessor.UpdateState applique les changements d’état. Le dernier état connu de chaque drone est stocké sous forme de document JSON dans Azure Cosmos DB. Étant donné que les drones envoient des mises à jour partielles, l’application ne peut pas simplement remplacer le document quand elle reçoit une mise à jour. Au lieu de cela, elle doit extraire l’état précédent, fusionner les champs, puis effectuer une opération upsert.

public class StateChangeProcessor : IStateChangeProcessor
{
    private IDocumentClient client;
    private readonly string cosmosDBDatabase;
    private readonly string cosmosDBCollection;

    public StateChangeProcessor(IDocumentClient client, IOptions<StateChangeProcessorOptions> options)
    {
        this.client = client;
        this.cosmosDBDatabase = options.Value.COSMOSDB_DATABASE_NAME;
        this.cosmosDBCollection = options.Value.COSMOSDB_DATABASE_COL;
    }

    public async Task<ResourceResponse<Document>> UpdateState(DeviceState source, ILogger log)
    {
        log.LogInformation("Processing change message for device ID {DeviceId}", source.DeviceId);

        DeviceState target = null;

        try
        {
            var response = await client.ReadDocumentAsync(UriFactory.CreateDocumentUri(cosmosDBDatabase, cosmosDBCollection, source.DeviceId),
                                                            new RequestOptions { PartitionKey = new PartitionKey(source.DeviceId) });

            target = (DeviceState)(dynamic)response.Resource;

            // Merge properties
            target.Battery = source.Battery ?? target.Battery;
            target.FlightMode = source.FlightMode ?? target.FlightMode;
            target.Latitude = source.Latitude ?? target.Latitude;
            target.Longitude = source.Longitude ?? target.Longitude;
            target.Altitude = source.Altitude ?? target.Altitude;
            target.AccelerometerOK = source.AccelerometerOK ?? target.AccelerometerOK;
            target.GyrometerOK = source.GyrometerOK ?? target.GyrometerOK;
            target.MagnetometerOK = source.MagnetometerOK ?? target.MagnetometerOK;
        }
        catch (DocumentClientException ex)
        {
            if (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                target = source;
            }
        }

        var collectionLink = UriFactory.CreateDocumentCollectionUri(cosmosDBDatabase, cosmosDBCollection);
        return await client.UpsertDocumentAsync(collectionLink, target);
    }
}

Ce code utilise l’interface IDocumentClient pour extraire un document à partir d’Azure Cosmos DB. Si le document existe, les nouvelles valeurs d’état sont fusionnées dans le document existant. Sinon, un nouveau document est créé. Les deux cas sont gérés par la méthode UpsertDocumentAsync.

Ce code est optimisé pour le cas où le document existe déjà et peut être fusionné. Lors du premier message de télémétrie provenant d’un drone donné, la méthode ReadDocumentAsync lève une exception car il n’existe aucun document pour ce drone. Après le premier message, le document sera disponible.

Notez que cette classe utilise l’injection de dépendances pour injecter le IDocumentClient pour Azure Cosmos DB et un IOptions<T> avec les paramètres de configuration. Nous verrons comment configurer l’injection de dépendances plus loin.

Notes

Azure Functions prend en charge une liaison de sortie pour Azure Cosmos DB. Cette liaison permet à l’application de fonction d’écrire des documents dans Azure Cosmos DB sans aucun code. Toutefois, la liaison de sortie ne fonctionnera pas pour ce scénario particulier, en raison de la logique d’upsert personnalisée nécessaire.

Gestion des erreurs

Comme mentionné plus haut, l’application de fonction RawTelemetryFunction traite un lot de messages dans une boucle. Cela signifie que la fonction doit gérer les exceptions correctement et continuer à traiter le reste du lot. Sinon, des messages risquent d’être perdus.

Si une exception se produit lors du traitement d’un message, la fonction place le message dans une file d’attente de lettres mortes :

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

La file d’attente de lettres mortes est définie à l’aide d’une liaison de sortie vers une file d’attente de stockage :

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

Ici, l’attribut Queue spécifie la liaison de sortie et l’attribut StorageAccount spécifie le nom d’un paramètre d’application qui contient la chaîne de connexion pour le compte de stockage.

Configuration de l’injection de dépendances

Le code suivant configure l’injection de dépendances pour la fonction 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<CosmosClient>(ctx => {
                var config = ctx.GetService<IConfiguration>();
                var cosmosDBEndpoint = config.GetValue<string>("CosmosDBEndpoint");
                return new CosmosClient(
                    accountEndpoint: cosmosDBEndpoint,
                    new DefaultAzureCredential());
            });
        }
    }
}

Les fonctions Azure écrites pour .NET peuvent utiliser le framework d’injection de dépendances ASP.NET Core. L’idée fondamentale est que vous déclarez une méthode de départ pour votre assembly. La méthode prend une interface IFunctionsHostBuilder, qui est utilisée afin de déclarer les dépendances pour l’injection de dépendances. Pour cela, vous devez appeler la méthode Add* sur l’objet Services. Quand vous ajoutez une dépendance, vous spécifiez sa durée de vie :

  • Les objets temporaires sont créés chaque fois qu’ils sont demandés.
  • Les objets à étendue sont créés une fois par exécution de la fonction.
  • Les objets singleton sont réutilisés lors des exécutions de la fonction, dans les limites de la durée de vie de l’hôte de fonction.

Dans cet exemple, les objets TelemetryProcessor et StateChangeProcessor sont déclarés comme temporaires. Cela convient pour les services légers et sans état. La classe DocumentClient, en revanche, doit être un singleton afin d’optimiser les performances. Pour plus d’informations, consultez Conseils sur les performances pour Azure Cosmos DB et .NET.

Si vous consultez le code de RawTelemetryFunction, vous verrez qu’il existe une autre dépendance qui n’apparaît pas dans le code de configuration de l’injection de dépendances, à savoir la classe TelemetryClient utilisée pour journaliser les métriques de l’application. Le runtime Azure Functions inscrit automatiquement cette classe dans le conteneur d’injection de dépendances ; vous n’avez donc pas besoin de l’inscrire explicitement.

Pour plus d’informations sur l’injection de dépendances dans Azure Functions, consultez les articles suivants :

Transmission des paramètres de configuration dans l’injection de dépendances

Parfois, un objet doit être initialisé avec des valeurs de configuration. En règle générale, ces paramètres doivent provenir de paramètres de l’application ou (dans le cas des secrets) d’Azure Key Vault.

Il existe deux exemples dans cette application. Tout d’abord, la classe DocumentClient prend un point de terminaison de service Azure Cosmos DB et une clé. Pour cet objet, l’application inscrit une expression lambda qui sera appelée par le conteneur d’injection de dépendances. Cette expression lambda utilise l’interface IConfiguration pour lire les valeurs de configuration :

builder.Services.AddSingleton<CosmosClient>(ctx => {
    var config = ctx.GetService<IConfiguration>();
    var cosmosDBEndpoint = config.GetValue<string>("CosmosDBEndpoint");
    return new CosmosClient(
                    accountEndpoint: cosmosDBEndpoint,
                    new DefaultAzureCredential());
});

Le deuxième exemple est la classe StateChangeProcessor. Pour cet objet, nous adoptons une approche appelée modèle d’options. Fonctionnement de l’opération :

  1. Définissez une classe T qui contient vos paramètres de configuration Ici, le nom de la collection et le nom de la base de données Azure Cosmos DB.

    public class StateChangeProcessorOptions
    {
        public string COSMOSDB_DATABASE_NAME { get; set; }
        public string COSMOSDB_DATABASE_COL { get; set; }
    }
    
  2. Ajoutez la classe T en tant que classe d’options pour l’injection de dépendances.

    builder.Services.AddOptions<StateChangeProcessorOptions>()
        .Configure<IConfiguration>((configSection, configuration) =>
        {
            configuration.Bind(configSection);
        });
    
  3. Dans le constructeur de la classe configurée, incluez un paramètre IOptions<T>.

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

Le système d’injection de dépendances renseignera automatiquement la classe d’options avec les valeurs de configuration et la transmettra au constructeur.

Cette approche offre plusieurs avantages :

  • Découplage de la classe par rapport à la source des valeurs de configuration.
  • Simplicité de définition de différentes sources de configuration, telles que des variables d’environnement ou des fichiers de configuration JSON.
  • Simplification des tests unitaires.
  • Utilisation d’une classe d’options fortement typée, qui est moins sujette aux erreurs que la simple transmission de valeurs scalaires.

Fonction GetStatus

L’autre application de fonction dans cette solution implémente une API REST simple pour obtenir le dernier état connu d’un drone. Cette fonction est définie dans une classe nommée GetStatusFunction. Voici le code complet de la fonction :

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

Cette fonction utilise un déclencheur HTTP pour traiter une requête HTTP GET. La fonction utilise une liaison d’entrée Azure Cosmos DB pour extraire le document demandé. Un facteur important est que cette liaison s’exécutera avant que la logique d’autorisation soit exécutée à l’intérieur de la fonction. Si un utilisateur non autorisé demande un document, la liaison de fonction extraira quand même le document. Le code d’autorisation renverra alors une erreur 401, et l’utilisateur ne verra pas le document. Le caractère acceptable de ce comportement peut dépendre de vos exigences. Par exemple, cette approche peut rendre plus difficile l’audit de l’accès aux données pour les données sensibles.

Authentification et autorisation

L’application web utilise Microsoft Entra ID pour authentifier les utilisateurs. Étant donné que l’application est une application monopage s’exécutant dans le navigateur, le flux de code d’autorisation est approprié :

  1. L’application web redirige l’utilisateur vers le fournisseur d’identité (en l’occurrence, Microsoft Entra ID).
  2. L’utilisateur entre ses informations d’identification.
  3. Le fournisseur d’identité redirige vers l’application web avec un code d’autorisation qui peut être échangé ultérieurement pour les jetons d’accès.
  4. L’application web envoie une requête à l’API web et inclut un jeton d’accès pour la ressource dans l’en-tête d’autorisation.

Diagramme de flux d’autorisation

Une application de fonction peut être configurée pour authentifier les utilisateurs sans aucun code. Pour plus d’informations, consultez la page Authentification et autorisation dans Azure App Service.

L’autorisation, quant à elle, nécessite généralement une logique métier. Microsoft Entra ID prend en charge l’authentification basée sur les revendications. Dans ce modèle, l’identité d’un utilisateur est représentée par un ensemble de revendications provenant du fournisseur d’identité. Une revendication peut être n’importe quel élément d’information sur l’utilisateur, comme son nom ou son adresse e-mail.

Le jeton d’accès contient un sous-ensemble des revendications d’utilisateur. Parmi celles-ci figurent les rôles d’application assignés à l’utilisateur.

Le paramètre principal de la fonction est un objet ClaimsPrincipal qui contient les revendications du jeton d’accès. Chaque revendication est une paire clé/valeur de type de revendication et de valeur de revendication. L’application les utilise pour autoriser la requête.

La méthode d’extension suivante teste si un objet ClaimsPrincipal contient un ensemble de rôles. Elle retourne false si l’un des rôles spécifiés est manquant. Si cette méthode retourne la valeur false, la fonction retourne HTTP 401 (Non autorisé).

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

Pour plus d’informations sur l’authentification et l’autorisation dans cette application, consultez la section Considérations en matière de sécurité de l’architecture de référence.

Étapes suivantes

Une fois que vous avez une idée de la façon dont cette solution de référence fonctionne, découvrez les bonnes pratiques et les recommandations pour des solutions similaires.

Azure Functions n’est que l’une des options de calcul Azure. Pour obtenir de l’aide sur le choix d’une technologie de calcul, consultez Choisir un service de calcul Azure pour votre application.