Tutorial de código: aplicación sin servidor con Functions

Azure Event Hubs
Azure Functions

Los modelos sin servidor abstraen el código de la infraestructura del proceso subyacente, lo que permite a los desarrolladores centrarse en la lógica de negocios sin una configuración minuciosa. El código sin servidor reduce los costos, ya que solo se paga por los recursos de la ejecución de código y su duración.

El modelo basado en eventos sin servidor se adapta a situaciones en las que un determinado evento desencadena una acción definida. Por ejemplo, la recepción de un mensaje entrante en el dispositivo desencadena el almacenamiento para su uso posterior o una actualización de la base de datos que necesita algún otro procesamiento.

Para ayudarle a explorar las tecnologías de Azure sin servidor en Azure, Microsoft ha desarrollado y probado una aplicación sin servidor que usa Azure Functions. Este artículo le guía a través del código de la solución de Functions sin servidor y describe las decisiones de diseño, los detalles de implementación y algunos de los problemas que puede encontrar.

Exploración de la solución

En esta solución de dos partes se describe un hipotético sistema de entrega con drones. Los drones envían el estado en curso a la nube, donde se almacenan estos mensajes para su uso posterior. Una aplicación web permite a los usuarios recuperar los mensajes para obtener el estado más reciente de los dispositivos.

Puede descargar el código de esta solución desde GitHub.

En este tutorial se da por supuesto que está familiarizado con las siguientes tecnologías:

No es necesario ser un experto en Functions o en Event Hubs, pero hay que comprender sus características a un alto nivel. Aquí le indicamos algunos estupendos recursos para comenzar:

Descripción del escenario

Diagram of the functional blocks

Fabrikam administra una flota de drones para un servicio de entrega de drones. La aplicación consta de dos áreas funcionales principales:

  • Ingesta de eventos. Durante el vuelo, los drones envían mensajes de estado a un punto de conexión en la nube. La aplicación ingiere y procesa estos mensajes, y escribe los resultados en una base de datos de back-end (Azure Cosmos DB). Los dispositivos envían mensajes en formato de búfer de protocolo (protobuf). Protobuf es un formato de serialización eficaz y autodescriptivo.

    Estos mensajes contienen actualizaciones parciales. En un intervalo fijo, cada dron envía un mensaje de "fotogramas clave" que contiene todos los campos de estado. Entre los fotogramas clave, los mensajes de estado solo incluyen campos que han cambiado desde el último mensaje. Este comportamiento es típico de muchos dispositivos de IoT que necesitan conservar el ancho de banda y la energía.

  • Aplicación web . Una aplicación web permite a los usuarios buscar un dispositivo y consultar su último estado conocido. Los usuarios deben iniciar sesión en la aplicación y autenticarse con Microsoft Entra ID. La aplicación solo permite las solicitudes de los usuarios que están autorizados a acceder a la aplicación.

Esta es una captura de pantalla de la aplicación web, que muestra el resultado de una consulta:

Screenshot of client app

Diseño de la aplicación

Fabrikam ha decidido utilizar Azure Functions para implementar la lógica de negocios de la aplicación. Azure Functions es un ejemplo de "Funciones como servicio" (FaaS). En este modelo informático, una función es un fragmento de código que se implementa en la nube y se ejecuta en un entorno de hospedaje. Este entorno de hospedaje abstrae completamente los servidores que ejecutan el código.

¿Por qué elegir un enfoque sin servidor?

Una arquitectura sin servidor con Functions es un ejemplo de una arquitectura basada en eventos. El código de función es desencadenado por algún evento que es externo a la función; en este caso, ya sea un mensaje de un dron o una petición HTTP de una aplicación cliente. Con una aplicación de función, no es necesario escribir ningún código para el desencadenador. Solo hay que escribir el código que se ejecuta en respuesta al desencadenador. Esto significa que puede concentrarse en la lógica de negocios, en lugar de escribir mucho código para tratar los problemas de infraestructura como la mensajería.

También hay algunas ventajas operativas para usar una arquitectura sin servidor:

  • No hay ninguna necesidad de administrar servidores.
  • Los recursos de proceso se asignan dinámicamente según sea necesario.
  • Se le cobrará solo por los recursos de proceso utilizados para ejecutar el código.
  • Los recursos de proceso se escalan a petición en función del tráfico.

Architecture

En el siguiente diagrama se muestra la arquitectura de alto nivel de la aplicación:

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

En un flujo de datos, las flechas muestran mensajes que van desde los dispositivos a Event Hubs y desencadenan la aplicación de funciones. En la aplicación, una flecha muestra mensajes no entregados que van a una cola de almacenamiento y otra muestra la escritura en Azure Cosmos DB. En otro flujo de datos, las flechas muestran la aplicación web cliente que obtiene archivos estáticos del hospedaje web estático de Blob Storage a través de una red CDN. Otra flecha muestra la solicitud HTTP del cliente que pasa a través de API Management. En API Management, una flecha muestra la aplicación de funciones que desencadena y lee los datos de Azure Cosmos DB. Otra flecha muestra la autenticación a través de Microsoft Entra ID. Un usuario también inicia sesión en Microsoft Entra ID.

Ingesta de eventos:

  1. Azure Event Hubs ingiere los mensajes de los drones.
  2. Event Hubs genera una secuencia de eventos que contienen los datos de los mensajes.
  3. Estos eventos desencadenan una aplicación de Azure Functions para procesarlos.
  4. Los resultados se almacenan en Azure Cosmos DB.

Aplicación web:

  1. Los archivos estáticos se atienden en la red CDN desde Blob Storage.
  2. El usuario inicia sesión en la aplicación web con Microsoft Entra ID.
  3. Azure API Management actúa como puerta de enlace que expone un punto de conexión de API REST.
  4. Las solicitudes HTTP del cliente desencadenan una aplicación de Azure Functions que lee en Azure Cosmos DB y devuelve el resultado.

Esta aplicación se basa en dos arquitecturas de referencia, correspondientes a los dos bloques funcionales descritos anteriormente:

Puede leer estos artículos para obtener más información sobre la arquitectura de alto nivel, los servicios Azure que se utilizan en la solución y las consideraciones de escalabilidad, seguridad y confiabilidad.

Función de telemetría de drones

Comencemos echando un vistazo a la función que procesa los mensajes de los drones en Event Hubs. La función se define en una clase denominada 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;
        }
    }
    ...
}

Esta clase tiene varias dependencias, que se insertan en el constructor mediante la inserción de dependencias:

  • En la siguiente lección, creará una clase auxiliar para simplificar el uso del patrón ITelemetryProcessor. Como veremos, estos objetos realizan la mayoría del trabajo.

  • TelemetryClient forma parte del SDK de Application Insights. Sirve para enviar métricas de aplicación personalizadas a Application Insights.

Más adelante, veremos cómo configurar la inserción de dependencias. De momento, supongamos que existen estas dependencias.

Configuración del desencadenador de Event Hubs

La lógica de la función se implementa como un método asincrónico denominado RunAsync. Esta es la firma del método:

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

El método toma los parámetros siguientes:

  • messages es una matriz de mensajes del centro de eventos.
  • deadLetterMessages es una cola de Azure Storage, utilizada para almacenar los mensajes fallidos.
  • logging proporciona una interfaz de registro, para escribir los registros de aplicaciones. Estos registros se envían a Azure Monitor.

El atributo EventHubTrigger en el parámetro messages configura el desencadenador. Las propiedades del atributo especifican un nombre de centro de eventos, una cadena de conexión y un grupo de consumidores. (Un grupo de consumidores es una vista aislada de la secuencia de eventos de Event Hubs. Esta abstracción permite varios consumidores del mismo centro de eventos).

Observe los signos de porcentaje (%) en algunas de las propiedades del atributo. Estos signos indican que la propiedad especifica el nombre de una configuración de la aplicación y el valor real se toma de esa configuración de la aplicación en tiempo de ejecución. En caso contrario, sin signos de porcentaje, la propiedad da el valor literal.

La propiedad Connection es una excepción. Esta propiedad siempre especifica un nombre de configuración de la aplicación, nunca un valor literal, por lo que el signo de porcentaje no es necesario. La razón de esta distinción es que una cadena de conexión es secreta y nunca debe comprobarse en el código fuente.

Mientras que las otras dos propiedades (nombre del centro de eventos y grupo de consumidores) no constituyen información confidencial como una cadena de conexión, es mejor ponerlas en la configuración de la aplicación, en lugar de codificarlos. De este modo, pueden actualizarse sin volver a compilar la aplicación.

Para más información acerca de cómo configurar este desencadenador, consulte Enlaces de Azure Event Hubs para Azure Functions.

Lógica de procesamiento de mensajes

Esta es la implementación del método RawTelemetryFunction.RunAsync que procesa un lote de mensajes:

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

Cuando se invoca la función, el parámetro messages contiene una matriz de mensajes desde el centro de eventos. El procesamiento de mensajes en lotes generalmente produce un mejor rendimiento que la lectura de un mensaje a la vez. Sin embargo, debe asegurarse de que la función sea resistente y trate los errores y excepciones correctamente. En caso contrario, si la función produce una excepción no controlada en medio de un lote, podría perder los mensajes restantes. Esta consideración se trata con más detalle en la sección Control de errores.

Pero si se omite el control de excepciones, la lógica de procesamiento para cada mensaje es sencilla:

  1. Llame a ITelemetryProcessor.Deserialize para deserializar el mensaje que contiene un cambio de estado del dispositivo.
  2. Llame a IStateChangeProcessor.UpdateState para procesar el cambio de estado.

Echemos un vistazo a estos dos métodos con más detalle, comenzando por el método Deserialize.

Método Deserialize

El método TelemetryProcess.Deserialize toma una matriz de bytes que contiene la carga del mensaje. Se deserializa esta carga y devuelve un objeto DeviceState, que representa el estado de un dron. El estado puede representar una actualización parcial, que contiene solo el delta del último estado conocido. Por lo tanto, el método necesita tratar null campos en la carga deserializada.

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

Otra clase auxiliar es la clase , que combina varias propiedades diferentes, entre las que se incluye el elemento de una cara y la clase , que es el rectángulo que define los límites de la cara. Los resultados se transforman en un modelo POCO más fácil de trabajar. Este diseño ayuda a aislar la lógica de procesamiento de los detalles de implementación de la serialización. La interfaz ITelemetrySerializer<T> se define en una biblioteca compartida, que también la utiliza el simulador de dispositivos para generar eventos simulados de dispositivos y enviarlos a Event Hubs.

using System;

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

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

Método UpdateState

El método StateChangeProcessor.UpdateState aplica los cambios de estado. El último estado conocido de cada dron se almacena como un documento JSON en Azure Cosmos DB. Debido a que los drones envían actualizaciones parciales, la aplicación no puede simplemente sobrescribir el documento cuando recibe una actualización. En su lugar, necesita recuperar el estado anterior, combinar los campos y después realizar una operación 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);
    }
}

Este código usa la interfaz IDocumentClient para capturar un documento de Azure Cosmos DB. Si el documento no existe, se combinan los nuevos valores de estado en el documento existente. En caso contrario, se crea un nuevo documento. Ambos casos se tratan mediante el método UpsertDocumentAsync.

Este código está optimizado para el caso en el que el documento ya existe y puede combinarse. En el primer mensaje de telemetría de un dron determinado, el método ReadDocumentAsync producirá una excepción, porque no hay ningún documento para ese dron. Después del primer mensaje, el documento estará disponible.

Observe que esta clase usa la inserción de dependencias para inyectar IDocumentClient para Azure Cosmos DB y un IOptions<T> con los ajustes de configuración. Veremos cómo configurar la inserción de dependencias más adelante.

Nota

Azure Functions admite un enlace de salida para Azure Cosmos DB. Este enlace permite a la aplicación de funciones escribir documentos en Azure Cosmos DB sin ningún código. Sin embargo, el enlace de salida no funcionará para este escenario en particular, debido a la lógica personalizada de upsert que se necesita.

Control de errores

Como se mencionó anteriormente, la función RawTelemetryFunction procesa un lote de mensajes en un bucle. Esto significa que la función necesita tratar cualquier excepción correctamente y continuar procesando el resto del lote. De lo contrario, es posible que se eliminen los mensajes.

Si se encuentra una excepción al procesar un mensaje, la función pone el mensaje en una cola de mensajes fallidos:

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 cola de mensajes fallidos se define mediante un enlace de salida a una cola de almacenamiento:

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

Aquí el atributo Queue especifica el enlace de salida y el atributo StorageAccount especifica el nombre de una configuración de aplicación que contiene la cadena de conexión para la cuenta de almacenamiento.

Tipo de implementación: En la plantilla de Resource Manager que crea la cuenta de almacenamiento, puede rellenar automáticamente una configuración de aplicación con la cadena de conexión. El truco consiste en usar la función listKeys.

Esta es la sección de la plantilla que crea la cuenta de almacenamiento para la cola:

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

Esta es la sección de la plantilla que crea la aplicación de función.


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

Esto define una configuración de aplicación denominada DeadLetterStorage cuyo valor se rellena mediante la función listKeys. Es importante hacer que el recurso de la aplicación de función dependa del recurso de la cuenta de almacenamiento (consulte el elemento dependsOn). Esto garantiza que la cuenta de almacenamiento se cree primero y que la cadena de conexión esté disponible.

Configuración de la inserción de dependencias

En el código siguiente se configura la inserción de dependencias para la función 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 escrito para .NET puede usar el marco de inserción de dependencias de ASP.NET Core. La idea básica es que se declara un método de inicio para el ensamblado. El método toma una interfaz IFunctionsHostBuilder, que se usa para declarar las dependencias para la inserción de dependencias. Esto se hace mediante una llamada al método Add* en el objeto Services. Cuando se agrega una dependencia, especifique su duración:

  • Se crean objetos transitorios cada vez que se solicitan.
  • Los objetos de ámbito se crean una vez por ejecución de la función.
  • Los objetos singleton se reutilizan entre las ejecuciones de funciones, dentro de la duración del host de función.

En este ejemplo, los objetos TelemetryProcessor y StateChangeProcessor se declaran como transitorios. Esto es apropiado para los servicios sin estado y ligeros. La clase DocumentClient, por otro lado, debe ser un singleton para mejorar el rendimiento. Para más información, consulte Sugerencias de rendimiento para Azure Cosmos DB y .NET.

Si se remite al código de RawTelemetryFunction, verá que hay otra dependencia que no aparece en el código de instalación de inserción de dependencias, a saber, la clase TelemetryClient que se utiliza para registrar las métricas de la aplicación. El entorno de ejecución de Functions registra automáticamente esta clase en el contenedor de inserción de dependencias, por lo que no es necesario registrarla explícitamente.

Para más información sobre la inserción de dependencias en Azure Functions, consulte los siguientes artículos:

Paso de valores de configuración en la inserción de dependencias

A veces debe inicializarse un objeto con algunos valores de configuración. Por lo general, esta configuración procede de la configuración de la aplicación o, en el caso de los secretos, de Azure Key Vault.

Hay dos ejemplos en esta aplicación. En primer lugar, la clase DocumentClient toma un punto de conexión de servicio de Azure Cosmos DB y una clave. Para este objeto, la aplicación registra una expresión lambda que invocará el contenedor de la inserción de dependencias. Esta expresión lambda usa la interfaz IConfiguration para leer los valores de configuración:

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

El segundo ejemplo es la clase StateChangeProcessor. Para este objeto, usamos un enfoque que se denomina patrón de opciones. Funcionamiento:

  1. Defina una clase T que contiene las opciones de configuración. En este caso, el nombre de la base de datos de Azure Cosmos DB y el nombre de la colección.

    public class StateChangeProcessorOptions
    {
        public string COSMOSDB_DATABASE_NAME { get; set; }
        public string COSMOSDB_DATABASE_COL { get; set; }
    }
    
  2. Agregue la clase T como una clase de opciones para la inserción de dependencias.

    builder.Services.AddOptions<StateChangeProcessorOptions>()
        .Configure<IConfiguration>((configSection, configuration) =>
        {
            configuration.Bind(configSection);
        });
    
  3. En el constructor de la clase que se está configurando, incluya un parámetro IOptions<T>.

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

El sistema de inserción de dependencias rellena automáticamente la clase de opciones con valores de configuración y lo pasa al constructor.

Hay varias ventajas en este enfoque:

  • Desacoplar la clase desde el origen de los valores de configuración.
  • Configurar fácilmente los distintos orígenes de la configuración, como las variables de entorno o los archivos de configuración JSON.
  • Simplificar las pruebas unitarias.
  • Utilizar una clase de opciones fuertemente tipadas, que es menos propensa a errores que simplemente pasar los valores escalares.

Función GetStatus

La otra aplicación de Functions de esta solución implementa una simple API REST para obtener el último estado conocido de un dron. Esta función se define en una clase denominada GetStatusFunction. Este es el código completo de la función:

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

Esta función utiliza un desencadenador HTTP para procesar una solicitud HTTP GET. La función utiliza un enlace de entrada de Azure Cosmos DB para recuperar el documento solicitado. Una consideración es que este enlace se ejecutará antes de que la lógica de autorización se realiza dentro de la función. Si un usuario no autorizado solicita un documento, el enlace de la función aún recuperará el documento. Después, el código de autorización devolverá un error 401, por lo que el usuario no verá el documento. Si este comportamiento es aceptable o no puede depender de sus requisitos. Por ejemplo, este enfoque podría dificultar el acceso a los datos de auditoría para la información confidencial.

Autenticación y autorización

La aplicación web usa Microsoft Entra ID para autenticar a los usuarios. Dado que la aplicación es una aplicación de página única (SPA) que se ejecuta en el explorador, el flujo de concesión implícita es apropiado:

  1. La aplicación web redirige al usuario al proveedor de identidades (en este caso, Microsoft Entra ID).
  2. El usuario especifica sus credenciales.
  3. El proveedor de identidades redirige de nuevo a la aplicación web con un token de acceso.
  4. La aplicación web envía una solicitud a la API web e incluye el token de acceso en el encabezado de autorización.

Implicit flow diagram

Se puede configurar una aplicación de función para autenticar usuarios con código cero. Para obtener más información, consulte Autenticación y autorización en Azure App Service.

La autorización, por otra parte, suele requerir cierta lógica de negocios. Microsoft Entra ID admite la autenticación basada en notificaciones. En este modelo, la identidad de un usuario se representa como un conjunto de notificaciones que proceden del proveedor de identidades. Una notificación puede ser cualquier información sobre el usuario, como su nombre o dirección de correo electrónico.

El token de acceso contiene un subconjunto de las notificaciones de usuario. Entre ellas se encuentran los roles de aplicación a los que está asignado el usuario.

El parámetro principal de la función es un objeto ClaimsPrincipal que contiene las notificaciones del token de acceso. Cada notificación es un par clave-valor de tipo y valor de notificación. La aplicación los utiliza para autorizar la solicitud.

El siguiente método de extensión comprueba si un objeto ClaimsPrincipal contiene un conjunto de roles. Devuelve false si falta cualquiera de los roles especificados. Si este método devuelve false, la función devuelve HTTP 401 (no autorizado).

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

Para obtener más información sobre la autenticación y autorización en esta aplicación, consulte la sección Consideraciones de seguridad de la arquitectura de referencia.

Pasos siguientes

Una vez que esté familiarizado con el funcionamiento de esta solución de referencia, aprenda los procedimientos recomendados y las recomendaciones para soluciones similares.

Azure Functions es solo una opción de proceso de Azure. Para obtener ayuda con la elección de una tecnología de proceso, consulte Elección de un servicio de proceso de Azure para la aplicación.