Instruções do código: Aplicação sem servidor com as Funções do AzureCode walkthrough: Serverless application with Azure Functions

Este artigo orienta o código para um aplicativo Web sem servidor que usa Azure Functions.This article walks through the code for a serverless web application that uses Azure Functions. também descreve as decisões de design, os detalhes da implementação e alguns dos “gotchas” que pode encontrar.It describes the design decisions, implementation details, and some of the "gotchas" that you might encounter.

Logótipo do GitHub O código fonte desta aplicação está disponível no GitHub.GitHub logo The source code for this application is available on GitHub.

Este artigo pressupõe um nível básico de familiaridade com as seguintes tecnologias:This article assumes a basic level of familiarity with the following technologies:

Não precisa de ser um especialista em Funções ou Hubs de Eventos, mas deve ter um elevado nível de compreensão das funcionalidades.You don't need to be an expert in Functions or Event Hubs, but you should understand their features at a high level. Veja a seguir alguns bons recursos para começar:Here are some good resources to get started:

O cenárioThe scenario

Diagrama dos blocos funcionais

A Fabrikam gere uma frota de drones de um serviço de entrega por drones.Fabrikam manages a fleet of drones for a drone delivery service. A aplicação é composta por duas áreas funcionais principais:The application consists of two main functional areas:

  • Ingestão de eventos.Event ingestion. Durante o voo, os drones enviam mensagens de estado para um ponto final da cloud.During flight, drones send status messages to a cloud endpoint. A aplicação ingere e processa estas mensagens e grava os resultados numa base de dados de back-end (Cosmos DB).The application ingests and processes these messages, and writes the results to a back-end database (Cosmos DB). Os dispositivos enviam mensagens no formato protobuf (protocol buffer).The devices send messages in protocol buffer (protobuf) format. O protobuf é um formato de serialização de descrição automática eficiente.Protobuf is an efficient, self-describing serialization format.

    Essas mensagens contêm atualizações parciais.These messages contain partial updates. Em intervalos fixos, cada drone envia uma mensagem de “fotograma chave” que contém todos os campos de estado.At a fixed interval, each drone sends a "key frame" message that contains all of the status fields. Entre os fotogramas chave, as mensagens de estado incluem apenas os campos que foram alterados desde a última mensagem.Between key frames, the status messages only include fields that changed since the last message. Este comportamento é típico de muitos dispositivos IoT que precisam de conservar largura de banda e energia.This behavior is typical of many IoT devices that need to conserve bandwidth and power.

  • Aplicação Web.Web app. Uma aplicação Web permite aos utilizadores procurar um dispositivo e consultar o último estado conhecido do dispositivo.A web application allows users to look up a device and query the device's last-known status. Os utilizadores devem iniciar sessão na aplicação e autenticar-se com o Azure Active Directory (Azure AD).Users must sign into the application and authenticate with Azure Active Directory (Azure AD). A aplicação permite apenas pedidos de utilizadores com autorização para aceder à aplicação.The application only allows requests from users who are authorized to access the app.

Veja a seguir uma captura de ecrã da aplicação Web a mostrar o resultado de uma consulta:Here's a screenshot of the web app, showing the result of a query:

Captura de ecrã da aplicação cliente

Conceber a aplicaçãoDesigning the application

A Fabrikam decidiu utilizar as Funções do Azure para implementar a lógica de negócio da aplicação.Fabrikam has decided to use Azure Functions to implement the application business logic. As Funções do Azure são um exemplo de “Funções como um Serviço” (FaaS).Azure Functions is an example of "Functions as a Service" (FaaS). Neste modelo de computação, uma função"* é um fragmento de código que é implementado na cloud e é executado num ambiente de alojamento.In this computing model, a function"* is a piece of code that is deployed to the cloud and runs in a hosting environment. Este ambiente de alojamento abstrai completamente os servidores que executam o código.This hosting environment completely abstracts the servers that run the code.

Por que deve escolher uma abordagem sem servidor?Why choose a serverless approach?

Uma arquitetura sem servidor com Funções é um exemplo de uma arquitetura condicionada por eventos.A serverless architecture with Functions is an example of an event-driven architecture. O código de função é acionado por eventos externos à função — neste caso, uma mensagem de um drone ou um pedido HTTP de uma aplicação cliente.The function code is a triggered by some event that's external to the function — in this case, either a message from a drone, or an HTTP request from a client application. Com uma aplicação de funções, não precisa de escrever código para o acionador.With a function app, you don't need to write any code for the trigger. Apenas escreve o código que é executado em resposta ao acionador.You only write the code that runs in response to the trigger. Tal significa que pode concentrar-se na lógica de negócio, ao invés de escrever uma grande quantidade de código para lidar com questões de infraestrutura, como as mensagens.That means you can focus on your business logic, rather than writing a lot of code to handle infrastructure concerns like messaging.

Existem também algumas vantagens operacionais na utilização de uma arquitetura sem servidor:There are also some operational advantages to using a serverless architecture:

  • Não é preciso gerir os servidores.There is no need to manage servers.
  • Os recursos de computação são atribuídos dinamicamente, conforme necessário.Compute resources are allocated dynamically as needed.
  • São cobrados apenas os recursos de computação utilizados para executar o código.You are charged only for the compute resources used to execute your code.
  • Os recursos de computação são dimensionados a pedido com base no tráfego.The compute resources scale on demand based on traffic.

ArquiteturaArchitecture

O diagrama seguinte mostra a arquitetura de alto nível da aplicação:The following diagram shows the high-level architecture of the application:

Arquitetura

Ingestão de eventos:Event ingestion:

  1. As mensagens dos drones são ingeridas pelos Hubs de Eventos do Azure.Drone messages are ingested by Azure Event Hubs.
  2. Os Hubs de Eventos produzem um fluxo de eventos que contém os dados das mensagens.Event Hubs produces a stream of events that contain the message data.
  3. Estes eventos acionam uma aplicação de Funções do Azure para as processar.These events trigger an Azure Functions app to process them.
  4. Os resultados são armazenados no Cosmos DB.The results are stored in Cosmos DB.

Aplicação Web:Web app:

  1. Os ficheiros estáticos são servidos pela CDN a partir do Armazenamento de blobs.Static files are served by CDN from Blob storage.
  2. O utilizador inicia sessão na aplicação Web com o Microsoft Azure AD.A user signs into the web app using Azure AD.
  3. A Gestão de API do Azure funciona como um gateway que expõe um ponto de final da API REST.Azure API Management acts as a gateway that exposes a REST API endpoint.
  4. Os pedidos HTTP do cliente acionam uma aplicação de Funções do Azure que lê a partir do Cosmos DB e devolve o resultado.HTTP requests from the client trigger an Azure Functions app that reads from Cosmos DB and returns the result.

Esta aplicação baseia-se em duas arquiteturas de referência, correspondentes aos dois blocos funcionais, descritos acima:This application is based on two reference architectures, corresponding to the two functional blocks described above:

Pode ler estes artigos para saber mais sobre a arquitetura de alto nível, os serviços do Azure utilizados na solução e as considerações sobre escalabilidade, segurança e fiabilidade.You can read those articles to learn more about the high-level architecture, the Azure services that are used in the solution, and considerations for scalability, security, and reliability.

Função de telemetria dos dronesDrone telemetry function

Vamos começar por examinar a função que processa as mensagens dos drones nos Hubs de Eventos.Let's start by looking at the function that processes drone messages from Event Hubs. A função é definida numa classe chamada RawTelemetryFunction:The function is defined in a class named 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 classe tem várias dependências, as quais são injetadas no construtor com a injeção de dependências:This class has several dependencies, which are injected into the constructor using dependency injection:

  • As interfaces ITelemetryProcessor e IStateChangeProcessor definem dois objetos do programa auxiliar.The ITelemetryProcessor and IStateChangeProcessor interfaces define two helper objects. Como vamos ver, estes objetos fazem a maior parte do trabalho.As we'll see, these objects do most of the work.

  • A TelemetryClient faz parte do SDK do Application Insights,The TelemetryClient is part of the Application Insights SDK. é utilizada para enviar métricas de aplicações personalizadas para o Application Insights.It is used to send custom application metrics to Application Insights.

Mais tarde, veremos como configurar a injeção de dependências.Later, we'll look at how to configure the dependency injection. Por enquanto, vamos assumir que estas dependências existem.For now, just assume these dependencies exist.

Configurar o acionador dos Hubs de EventosConfigure the Event Hubs trigger

A lógica na função é implementada como um método assíncrono chamado RunAsync.The logic in the function is implemented as an asynchronous method named RunAsync. Esta é a assinatura do método:Here is the method signature:

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

O método recebe os seguintes parâmetros:The method takes the following parameters:

  • messages é uma matriz das mensagens do hub de eventos.messages is an array of event hub messages.
  • deadLetterMessages é uma Fila de Armazenamento do Microsoft Azure, utilizada para armazenar mensagens não entregues.deadLetterMessages is an Azure Storage Queue, used for storing dead letter messages.
  • logging fornece uma interface de registo, para escrever registos de aplicações.logging provides a logging interface, for writing application logs. Estes registos são enviados para o Azure Monitor.These logs are sent to Azure Monitor.

O atributo EventHubTrigger no parâmetro messages configura o acionador.The EventHubTrigger attribute on the messages parameter configures the trigger. As propriedades do atributo especificam um nome do hub de eventos, uma cadeia de ligação e um grupo de consumidoresThe properties of the attribute specify an event hub name, a connection string, and a consumer group. (um grupo de consumidores é uma vista isolada do fluxo de eventos dos Hubs de Eventos.(A consumer group is an isolated view of the Event Hubs event stream. Esta abstração permite vários consumidores do mesmo hub de eventos).This abstraction allows for multiple consumers of the same event hub.)

Repare nos sinais de percentagem (%) em algumas das propriedades dos atributos.Notice the percent signs (%) in some of the attribute properties. Estes indicam que a propriedade especifica o nome de uma definição de aplicação e o valor real é retirado dessa definição de aplicação no tempo de execução.These indicate that the property specifies the name of an app setting, and the actual value is taken from that app setting at run time. Caso contrário, sem os símbolos de percentagem, a propriedade fornece o valor literal.Otherwise, without percent signs, the property gives the literal value.

A propriedade Connection é uma exceção.The Connection property is an exception. Esta propriedade especifica sempre um nome de definição de aplicação, nunca um valor literal, pelo que o símbolo de percentagem não é necessário.This property always specifies an app setting name, never a literal value, so the percent sign is not needed. O motivo para essa distinção é que uma cadeia de ligação é secreta e nunca deve ser verificada no código fonte.The reason for this distinction is that a connection string is secret and should never be checked into source code.

Embora as outras duas propriedades (nome do hub de eventos e grupo de consumidores) não sejam dados confidenciais como uma cadeia de ligação, continua a ser melhor colocá-las nas definições de aplicação, em vez de no hard-coding.While the other two properties (event hub name and consumer group) are not sensitive data like a connection string, it's still better to put them into app settings, rather than hard coding. Desta forma, podem ser atualizadas sem recompilar a aplicação.That way, they can be updated without recompiling the app.

Para obter mais informações acerca da configuração deste acionador, veja Enlaces dos Hubs de Eventos do Azure para as Funções do Azure.For more information about configuring this trigger, see Azure Event Hubs bindings for Azure Functions.

Lógica de processamento das mensagensMessage processing logic

Veja a seguir a implementação do método RawTelemetryFunction.RunAsync que processa um lote de mensagens:Here's the implementation of the RawTelemetryFunction.RunAsync method that processes a batch of 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 });
        }
    }
}

Quando a função é invocada, o parâmetro messages contém uma matriz de mensagens do hub de eventos.When the function is invoked, the messages parameter contains an array of messages from the event hub. O processamento de mensagens em lotes produzirá, em geral, um melhor desempenho do que a leitura de uma mensagem de cada vez.Processing messages in batches will generally yield better performance than reading one message at a time. No entanto, tem de confirmar que a função é resiliente e que processa as falhas e as exceções corretamente.However, you have to make sure the function is resilient and handles failures and exceptions gracefully. Caso contrário, se a função emitir uma exceção não processada no meio de um lote, poderá perder as restantes mensagens.Otherwise, if the function throws an unhandled exception in the middle of a batch, you might lose the remaining messages. Esta consideração é abordada mais detalhadamente na seção Processamento de erros.This consideration is discussed in more detail in the section Error handling.

Porém, se ignorar o processamento de exceções, a lógica de processamento para cada mensagem é simples:But if you ignore the exception handling, the processing logic for each message is simple:

  1. Chame ITelemetryProcessor.Deserialize para anular a serialização da mensagem que contém uma alteração de estado do dispositivo.Call ITelemetryProcessor.Deserialize to deserialize the message that contains a device state change.
  2. Chame IStateChangeProcessor.UpdateState para processar a alteração de estado.Call IStateChangeProcessor.UpdateState to process the state change.

Vamos examinar estes dois métodos em maior detalhe, a começar pelo método Deserialize.Let's look at these two methods in more detail, starting with the Deserialize method.

Método de anulação da serializaçãoDeserialize method

O método TelemetryProcess.Deserialize recorre a uma matriz de bytes que contém o payload das mensagens.The TelemetryProcess.Deserialize method takes a byte array that contains the message payload. O método anula a serialização deste payload e devolve um objeto DeviceState, que representa o estado de um drone.It deserializes this payload and returns a DeviceState object, which represents the state of a drone. O estado pode representar uma atualização parcial, que contém apenas o delta do último estado conhecido.The state may represent a partial update, containing just the delta from the last-known state. Assim, o método precisa de processar os campos null no payload da serialização anulada.Therefore, the method needs to handle null fields in the deserialized 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;
    }
}

Este método utiliza outra interface de programa auxiliar, ITelemetrySerializer<T>, para anular a serialização da mensagem não processada.This method uses another helper interface, ITelemetrySerializer<T>, to deserialize the raw message. Os resultados são, em seguida, transformados num modelo POCO, mais fácil trabalhar.The results are then transformed into a POCO model that is easier to work with. Este design ajuda a isolar a lógica de processamento dos detalhes de implementação da serialização.This design helps to isolate the processing logic from the serialization implementation details. A interface ITelemetrySerializer<T> é definida numa biblioteca partilhada, que também é utilizada pelo simulador de dispositivos para gerar eventos de dispositivos simulados e enviá-los para os Hubs de Eventos.The ITelemetrySerializer<T> interface is defined in a shared library, which is also used by the device simulator to generate simulated device events and send them to Event Hubs.

using System;

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

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

Método UpdateStateUpdateState method

O método StateChangeProcessor.UpdateState aplica as alterações de estado.The StateChangeProcessor.UpdateState method applies the state changes. O último estado conhecido de cada drone é armazenado como um documento JSON no Cosmos DB.The last-known state for each drone is stored as a JSON document in Cosmos DB. Uma vez que os drones enviam atualizações parciais, a aplicação não pode simplesmente substituir o documento quando recebe uma atualização.Because the drones send partial updates, the application can't simply overwrite the document when it gets an update. Em vez disso, precisa de obter o estado anterior, unir os campos e, em seguida, realizar uma operação de upsert.Instead, it needs to fetch the previous state, merge the fields, and then perform an upsert operation.

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 utiliza a interface IDocumentClient para obter um documento do Cosmos DB.This code uses the IDocumentClient interface to fetch a document from Cosmos DB. Se o documento existir, os valores do novo estado serão unidos no documento existente.If the document exists, the new state values are merged into the existing document. Caso contrário, será criado um novo documento.Otherwise, a new document is created. Ambos os casos são processados pelo método UpsertDocumentAsync.Both cases are handled by the UpsertDocumentAsync method.

Esse código é otimizado para o caso onde o documento já existe e pode ser unido.This code is optimized for the case where the document already exists and can be merged. Na primeira mensagem de telemetria de um determinado drone, o método ReadDocumentAsync vai gerar uma exceção, porque não existe nenhum documento para esse drone.On the first telemetry message from a given drone, the ReadDocumentAsync method will throw an exception, because there is no document for that drone. Após a primeira mensagem, o documento vai estar disponível.After the first message, the document will be available.

Observe que esta classe utiliza a injeção de dependências para injetar o IDocumentClient para o Cosmos DB e IOptions<T> com as definições de configuração.Notice that this class uses dependency injection to inject the IDocumentClient for Cosmos DB and an IOptions<T> with configuration settings. Veremos como configurar a injeção de dependências mais tarde.We'll see how to set up the dependency injection later.

Nota

As Funções do Azure suportam um enlace de saída para o Cosmos DB.Azure Functions supports an output binding for Cosmos DB. Este enlace permite à aplicação de funções escrever documentos no Cosmos DB sem necessidade de código.This binding lets the function app write documents in Cosmos DB without any code. No entanto, o enlace de saída não funcionará neste cenário específico, devido à lógica de upsert personalizada necessária.However, the output binding won't work for this particular scenario, because of the custom upsert logic that's needed.

Processamento de errosError handling

Conforme mencionado anteriormente, a aplicação de funções RawTelemetryFunction processa um lote de mensagens num ciclo.As mentioned earlier, the RawTelemetryFunction function app processes a batch of messages in a loop. Tal significa que a função precisa de processar corretamente todas as exceções e continuar a processar o restante lote.That means the function needs to handle any exceptions gracefully and continue processing the rest of the batch. Caso contrário, as mensagens podem ser removidas.Otherwise, messages might get dropped.

Se for encontrada uma exceção ao processar uma mensagem, a função colocará a mensagem numa fila de mensagens não entregues:If an exception is encountered when processing a message, the function puts the message onto a dead-letter queue:

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

A fila de mensagens não entregues é definida com um enlace de saída para uma fila de armazenamento:The dead-letter queue is defined using an output binding to a storage queue:

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

Aqui, o atributo Queue especifica o enlace de saída e o atributo StorageAccount especifica o nome de uma definição de aplicação que contém a cadeia de ligação da conta de armazenamento.Here the Queue attribute specifies the output binding, and the StorageAccount attribute specifies the name of an app setting that holds the connection string for the storage account.

Tipo de implementação: no modelo do Resource Manager que cria a conta de armazenamento, pode preencher automaticamente uma definição de aplicação com a cadeia de ligação.Deployment tip: In the Resource Manager template that creates the storage account, you can automatically populate an app setting with the connection string. O truque consiste em utilizar a função listkeys.The trick is to use the listkeys function.

Esta é a secção do modelo que cria a conta de armazenamento para a fila:Here is the section of the template that creates the storage account for the queue:

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

Esta é a seção do modelo que cria a aplicação de funções.Here is the section of the template that creates the function app.


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

Esta define uma definição de aplicação denominada DeadLetterStorage, cujo valor é preenchido com a função listkeys.This defines an app setting named DeadLetterStorage whose value is populated using the listkeys function. É importante tornar o recurso da aplicação de funções dependente do recurso da conta de armazenamento (veja o elemento dependsOn).It's important to make the function app resource depend on the storage account resource (see the dependsOn element). Tal garante que a conta de armazenamento é criada primeiro e que a cadeia de ligação está disponível.This guarantees that the storage account is created first and the connection string is available.

Configurar a injeção de dependênciasSetting up dependency injection

O código a seguir configura a injeção de dependências para a função RawTelemetryFunction:The following code sets up dependency injection for the RawTelemetryFunction function:

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

As Funções do Azure escritas para .NET podem utilizar a arquitetura de injeção de dependências do ASP.NET Core.Azure Functions written for .NET can use the ASP.NET Core dependency injection framework. A ideia geral é que declare um método de arranque para a sua assemblagem.The basic idea is that you declare a startup method for your assembly. O método utiliza uma interface IFunctionsHostBuilder, que é utilizada para declarar as dependências para a injeção de dependências.The method takes an IFunctionsHostBuilder interface, which is used to declare the dependencies for DI. Pode fazê-lo ao chamar o método Add* no objeto Services.You do this by calling Add* method on the Services object. Quando adiciona uma dependência, especifica a sua duração:When you add a dependency, you specify its lifetime:

  • Os objetos transitórios são criados sempre que são pedidos.Transient objects are created each time they're requested.
  • Os objetos com âmbito são criados uma vez por cada execução de função.Scoped objects are created once per function execution.
  • Os objetos singleton são reutilizados entre execuções de função, dentro da duração do anfitrião da função.Singleton objects are reused across function executions, within the lifetime of the function host.

Neste exemplo, os objetos TelemetryProcessor e StateChangeProcessor são declarados como transitórios,In this example, the TelemetryProcessor and StateChangeProcessor objects are declared as transient. o que é adequado para os serviços sem estado simples.This is appropriate for lightweight, stateless services. A classe DocumentClient, por outro lado, deve ser singleton para um melhor desempenho.The DocumentClient class, on the other hand, should be a singleton for best performance. Para obter mais informações, veja Sugestões de desempenho para o Azure Cosmos DB e .NET.For more information, see Performance tips for Azure Cosmos DB and .NET.

Se consultar novamente o código da RawTelemetryFunction, poderá ver que existe outra dependência que não aparece no código de configuração da injeção de dependências, isto é, a classe TelemetryClient que é utilizada para registar as métricas da aplicação.If you refer back to the code for the RawTelemetryFunction, you'll see there another dependency that doesn't appear in DI setup code, namely the TelemetryClient class that is used to log application metrics. O runtime das Funções regista automaticamente esta classe no contentor de injeção de dependências, pelo que não tem de o registar explicitamente.The Functions runtime automatically registers this class into the DI container, so you don't need to register it explicitly.

Para obter mais informações sobre a injeção de dependências nas Funções do Azure, veja os seguintes artigos:For more information about DI in Azure Functions, see the following articles:

Transmitir definições de configuração na injeção de dependênciasPassing configuration settings in DI

Por vezes, um objeto tem de ser inicializado com alguns valores de configuração.Sometimes an object must be initialized with some configuration values. Por norma, estas definições devem vir das definições de aplicação ou (no caso de segredos) do Azure Key Vault.Generally, these settings should come from app settings or (in the case of secrets) from Azure Key Vault.

Existem dois exemplos nesta aplicação.There are two examples in this application. Primeiro, a classe DocumentClient utiliza um ponto final de serviço e uma chave do Cosmos DB.First, the DocumentClient class takes a Cosmos DB service endpoint and key. Para este objeto, a aplicação regista um operador lambda que será invocado pelo contentor de injeção de dependências.For this object, the application registers a lambda that will be invoked by the DI container. Este operador lambda utiliza a interface IConfiguration para ler os valores de configuração:This lambda uses the IConfiguration interface to read the configuration values:

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

O segundo exemplo é a classe StateChangeProcessor.The second example is the StateChangeProcessor class. Para este objeto, utilizamos uma abordagem denominada padrão de opções.For this object, we use an approach called the options pattern. Eis como funciona:Here's how it works:

  1. Defina uma classe T que contém as definições de configuração.Define a class T that contains your configuration settings. Neste caso, o nome da base de dados e o nome da coleção do Cosmos DB.In this case, the Cosmos DB database name and collection name.

    public class StateChangeProcessorOptions
    {
        public string COSMOSDB_DATABASE_NAME { get; set; }
        public string COSMOSDB_DATABASE_COL { get; set; }
    }
    
  2. Adicione a classe T como uma classe de opções para a injeção de dependências.Add the class T as an options class for DI.

    builder.Services.AddOptions<StateChangeProcessorOptions>()
        .Configure<IConfiguration>((configSection, configuration) =>
        {
            configuration.Bind(configSection);
        });
    
  3. No construtor da classe a ser configurada, inclua um parâmetro IOptions<T>.In the constructor of the class that is being configured, include an IOptions<T> parameter.

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

O sistema de injeção de dependências vai preencher automaticamente a classe de opções com os valores de configuração e transmiti-la ao construtor.The DI system will automatically populate the options class with configuration values and pass this to the constructor.

Existem diversas vantagens nesta abordagem:There are several advantages of this approach:

  • Desassociar a classe da origem dos valores de configuração.Decouple the class from the source of the configuration values.
  • Configurar facilmente as diferentes origens de configuração, como variáveis de ambiente ou ficheiros de configuração JSON.Easily set up different configuration sources, such as environment variables or JSON configuration files.
  • Simplificar o teste de unidades.Simplify unit testing.
  • Utilizar uma classe de opções com tipos de dados inflexíveis, que é menos propensa a erros do que apenas a transmissão de valores escalares.Use a strongly typed options class, which is less error prone than just passing in scalar values.

Função GetStatusGetStatus function

A outra aplicação de Funções nesta solução implementa uma API REST simples para obter o último estado conhecido de um drone.The other Functions app in this solution implements a simple REST API to get the last-known status of a drone. Esta função é definida numa classe chamada GetStatusFunction.This function is defined in a class named GetStatusFunction. Veja a seguir o código completo da função:Here is the complete code for the function:

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 função utiliza um acionador HTTP para processar um pedido HTTP GET.This function uses an HTTP trigger to process an HTTP GET request. A função utiliza um enlace de entrada do Cosmos DB para obter o documento pedido.The function uses a Cosmos DB input binding to fetch the requested document. Uma consideração a ter em conta é que este enlace será executado antes da lógica de autorização ser realizada dentro da função.One consideration is that this binding will run before the authorization logic is performed inside the function. Se um utilizador não autorizado solicitar um documento, o enlace da função ainda poderá obter o documento.If an unauthorized user requests a document, the function binding will still fetch the document. Em seguida, o código de autorização devolverá um erro 401, para que o utilizador não veja o documento.Then the authorization code will return a 401, so the user won't see the document. A decisão de considerar este comportamento aceitável ou não pode depender dos seus requisitos.Whether this behavior is acceptable may depend on your requirements. Por exemplo, esta abordagem pode tornar mais difícil auditar o acesso a dados confidenciais.For example, this approach might make it harder to audit data access for sensitive data.

Autenticação e autorizaçãoAuthentication and authorization

A aplicação Web utiliza o Microsoft Azure AD para autenticar os utilizadores.The web app uses Azure AD to authenticate users. Uma vez que a aplicação é uma aplicação de página única (SPA) em execução no browser, o fluxo de concessão implícita é apropriado:Because the app is a single-page application (SPA) running in the browser, the implicit grant flow is appropriate:

  1. A aplicação Web redireciona o utilizador para o fornecedor de identidade (neste caso, o Microsoft Azure AD).The web app redirects the user to the identity provider (in this case, Azure AD).
  2. O utilizador introduz as credenciais.The user enters their credentials.
  3. O fornecedor de identidade redireciona de volta para a aplicação Web com um token de acesso.The identity provider redirects back to the web app with an access token.
  4. A aplicação Web envia um pedido para a API Web e inclui o token de acesso no Cabeçalho de autorização.The web app sends a request to the web API and includes the access token in the Authorization header.

Diagrama do fluxo implícito

Uma aplicação de Função pode ser configurada para autenticar os utilizadores com o código zero.A Function application can be configured to authenticate users with zero code. Para obter mais informações, veja Autenticação e autorização no Serviço de Aplicações do Azure.For more information, see Authentication and authorization in Azure App Service.

Por outro lado, a autorização geralmente requer alguma lógica de negócio.Authorization, on the other hand, generally requires some business logic. O Microsoft Azure AD suporta a autenticação baseada em afirmações.Azure AD supports claims based authentication. Neste modelo, a identidade de um utilizador é representada como um conjunto de afirmações provenientes do fornecedor de identidade.In this model, a user's identity is represented as a set of claims that come from the identity provider. Uma afirmação pode ser qualquer informação sobre o utilizador, como o nome ou o endereço de e-mail.A claim can be any piece of information about the user, such as their name or email address.

O token de acesso contém um subconjunto de afirmações do utilizador.The access token contains a subset of user claims. Entre estas estão as funções de aplicação que são atribuídas ao utilizador.Among these are any application roles that the user is assigned to.

O parâmetro principal da função é um objeto ClaimsPrincipal que contém as afirmações do token de acesso.The principal parameter of the function is a ClaimsPrincipal object that contains the claims from the access token. Cada afirmação é um par de chaves/valores do tipo de afirmação e valor de afirmação.Each claim is a key/value pair of claim type and claim value. A aplicação utiliza-os para autorizar o pedido.The application uses these to authorize the request.

O seguinte método de extensão testa se um objeto ClaimsPrincipal contém um conjunto de funções.The following extension method tests whether a ClaimsPrincipal object contains a set of roles. Este devolve false caso alguma das funções especificadas esteja em falta.It returns false if any of the specified roles is missing. Se esse método devolver o valor falso, a função devolverá HTTP 401 (Não Autorizado).If this method returns false, the function returns HTTP 401 (Unauthorized).

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 obter mais informações sobre a autenticação e a autorização nesta aplicação, veja a secção Considerações de segurança da arquitetura de referência.For more information about authentication and authorization in this application, see the Security considerations section of the reference architecture.

Passos seguintesNext steps