Manipuladores personalizados do Azure Functions

Cada Aplicativo de funções é executado por um manipulador de linguagem específica. Embora o Azure Functions ofereça suporte a muitos manipuladores de linguagem por padrão, há casos em que você talvez queira usar outras linguagens ou runtimes.

Os manipuladores personalizados são servidores Web leves que recebem eventos do host do Functions. Qualquer linguagem que ofereça suporte a primitivos HTTP pode implementar um manipulador personalizado.

Os manipuladores personalizados são mais adequados para situações em que você deseja:

  • Implementar um aplicativo de funções em uma linguagem que não tenha suporte atualmente, como Go ou Rust.
  • Implementar um aplicativo de funções em um runtime que não tenha suporte atualmente, como Deno.

Com manipuladores personalizados, você pode usar gatilhos e associações de entrada e saída por meio de pacotes de extensão.

Introdução a manipuladores personalizados do Azure Functions com os guias de início rápido em Go e Rust.

Visão geral

O diagrama a seguir mostra a relação entre o host do Functions e um servidor Web implementado como um manipulador personalizado.

Azure Functions custom handler overview

  1. Cada evento dispara uma solicitação enviada ao host do Functions. Um evento é qualquer disparador com suporte do Azure Functions.
  2. O host do Functions emite, então, um payload de solicitação para o servidor Web. O payload contém dados de associação de entrada e de gatilho e outros metadados para a função.
  3. O servidor Web executa a função individual e retorna um payload de resposta para o host do Functions.
  4. O host do Functions passa dados da resposta para as associações de saída da função para processamento.

Um aplicativo Azure Functions implementado como um manipulador personalizado deve configurar arquivos host.json, local.settings.jsone function.json de acordo com algumas convenções.

Estrutura de aplicativo

Para implementar um manipulador personalizado, você precisa dos seguintes aspectos para seu aplicativo:

  • Um arquivo host.json na raiz do seu aplicativo
  • Um arquivo local.settings.json na raiz do seu aplicativo
  • Um arquivo function.json para cada função (dentro de uma pasta que corresponde ao nome da função)
  • Um comando, script ou executável, que execute um servidor Web

O diagrama a seguir mostra como esses arquivos examinam o sistema de arquivos em busca de uma função chamada "MyQueueFunction" e um executável do manipulador personalizado chamado handler.exe.

| /MyQueueFunction
|   function.json
|
| host.json
| local.settings.json
| handler.exe

Configuração

O aplicativo é configurado por meio dos arquivos host.json e local.settings.json.

host.json

O host.json informa ao host do Functions para onde enviar solicitações ao apontar para um servidor Web capaz de processar eventos HTTP.

Um manipulador personalizado é definido com a configuração do arquivo host.json com detalhes sobre como executar o servidor Web por meio da seção customHandler.

{
  "version": "2.0",
  "customHandler": {
    "description": {
      "defaultExecutablePath": "handler.exe"
    }
  }
}

A seção customHandler aponta para um destino, conforme definido por defaultExecutablePath. O destino de execução pode ser um comando, um executável ou um arquivo no qual o servidor Web seja implementado.

Use a matriz arguments para passar quaisquer argumentos para o executável. Os argumentos dão suporte à expansão de variáveis de ambiente (configurações do aplicativo) usando a notação %%.

Você também pode alterar o diretório de trabalho usado pelo executável com workingDirectory.

{
  "version": "2.0",
  "customHandler": {
    "description": {
      "defaultExecutablePath": "app/handler.exe",
      "arguments": [
        "--database-connection-string",
        "%DATABASE_CONNECTION_STRING%"
      ],
      "workingDirectory": "app"
    }
  }
}
Suporte a associações

Os gatilhos padrão, juntamente com as associações de entrada e saída, estão disponíveis por meio da referência de pacotes de extensão em seu arquivo host.json.

local.settings.json

O local.settings.json define as configurações do aplicativo usadas ao executar o aplicativo de funções localmente. Como ele pode conter segredos, o local.settings.json deve ser excluído do controle do código-fonte. No Azure, use as configurações de aplicativo em vez disso.

Para manipuladores personalizados, defina FUNCTIONS_WORKER_RUNTIME como Custom no local.settings.json.

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "Custom"
  }
}

Metadados de função

Quando usado com um manipulador personalizado, o conteúdo do function.json não é diferente de como você definiria uma função em qualquer outro contexto. O único requisito é que arquivos function.json devem estar em uma pasta com um nome que corresponda ao nome da função.

O function.json a seguir configura uma função que tem um gatilho de fila e uma associação de saída de fila. Por estar em uma pasta chamada MyQueueFunction, ele define uma função chamada MyQueueFunction.

MyQueueFunction/function.json

{
  "bindings": [
    {
      "name": "myQueueItem",
      "type": "queueTrigger",
      "direction": "in",
      "queueName": "messages-incoming",
      "connection": "AzureWebJobsStorage"
    },
    {
      "name": "$return",
      "type": "queue",
      "direction": "out",
      "queueName": "messages-outgoing",
      "connection": "AzureWebJobsStorage"
    }
  ]
}

Payload da solicitação

Quando uma mensagem da fila é recebida, o host do Functions envia uma solicitação de postagem HTTP para o manipulador personalizado com um payload no corpo.

O código a seguir representa um exemplo de payload de solicitação. O payload inclui uma estrutura JSON com dois membros: Data e Metadata.

O membro Data inclui chaves que correspondem aos nomes de entrada e de gatilho, conforme definido na matriz de associações no arquivo function.json.

O membro Metadata inclui metadados gerados da origem do evento.

{
  "Data": {
    "myQueueItem": "{ message: \"Message sent\" }"
  },
  "Metadata": {
    "DequeueCount": 1,
    "ExpirationTime": "2019-10-16T17:58:31+00:00",
    "Id": "800ae4b3-bdd2-4c08-badd-f08e5a34b865",
    "InsertionTime": "2019-10-09T17:58:31+00:00",
    "NextVisibleTime": "2019-10-09T18:08:32+00:00",
    "PopReceipt": "AgAAAAMAAAAAAAAAAgtnj8x+1QE=",
    "sys": {
      "MethodName": "QueueTrigger",
      "UtcNow": "2019-10-09T17:58:32.2205399Z",
      "RandGuid": "24ad4c06-24ad-4e5b-8294-3da9714877e9"
    }
  }
}

Payload de resposta

Por convenção, as respostas de função são formatadas como pares chave/valor. As chaves com suporte incluem:

Chave de payload Tipo de dados Comentários
Outputs objeto Mantém valores de resposta conforme definido pela matriz bindings no function.json.

Por exemplo, se uma função for configurada com uma associação de saída de fila chamada "myQueueOutput", então Outputs conterá uma chave chamada myQueueOutput, que será definida pelo manipulador personalizado para as mensagens que são enviadas para a fila.
Logs array As mensagens aparecem nos logs de invocação do Functions.

Quando em execução no Azure, as mensagens aparecem no Application Insights.
ReturnValue string Usado para fornecer uma resposta quando uma saída é configurada como $return no arquivo function.json.

Esse é um exemplo de um payload de resposta.

{
  "Outputs": {
    "res": {
      "body": "Message enqueued"
    },
    "myQueueOutput": [
      "queue message 1",
      "queue message 2"
    ]
  },
  "Logs": [
    "Log message 1",
    "Log message 2"
  ],
  "ReturnValue": "{\"hello\":\"world\"}"
}

Exemplos

Os manipuladores personalizados podem ser implementados em qualquer linguagem que dê suporte ao recebimento de eventos HTTP. Os exemplos a seguir mostram como implementar um manipulador personalizado usando a linguagem de programação Go.

Função com associações

O cenário implementado nesse exemplo apresenta uma função chamada order que aceita um POST com um payload que representa um pedido de produto. Como um pedido é postado na função, uma mensagem de Armazenamento de Filas é criada e uma resposta HTTP é retornada.

Implementação

Em uma pasta chamada Pedido, o arquivo function.json configura a função disparada por HTTP.

order/function.json

{
  "bindings": [
    {
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["post"]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    },
    {
      "type": "queue",
      "name": "message",
      "direction": "out",
      "queueName": "orders",
      "connection": "AzureWebJobsStorage"
    }
  ]
}

Essa função é definida como uma função disparada por HTTP que retorna uma resposta HTTP e gera uma mensagem de Armazenamento de fila.

Na raiz do aplicativo, o arquivo host.json é configurado para executar um arquivo executável chamado handler.exe (handler no Linux ou macOS).

{
  "version": "2.0",
  "customHandler": {
    "description": {
      "defaultExecutablePath": "handler.exe"
    }
  },
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[1.*, 2.0.0)"
  }
}

Essa é a solicitação HTTP enviada ao runtime do Functions.

POST http://127.0.0.1:7071/api/order HTTP/1.1
Content-Type: application/json

{
  "id": 1005,
  "quantity": 2,
  "color": "black"
}

Em seguida, o runtime do Functions enviará a seguinte solicitação HTTP para o manipulador personalizado:

POST http://127.0.0.1:<FUNCTIONS_CUSTOMHANDLER_PORT>/order HTTP/1.1
Content-Type: application/json

{
  "Data": {
    "req": {
      "Url": "http://localhost:7071/api/order",
      "Method": "POST",
      "Query": "{}",
      "Headers": {
        "Content-Type": [
          "application/json"
        ]
      },
      "Params": {},
      "Body": "{\"id\":1005,\"quantity\":2,\"color\":\"black\"}"
    }
  },
  "Metadata": {
  }
}

Observação

Algumas partes do payload foram removidas para fins de brevidade.

O handler.exe é o programa do manipulador personalizado Go compilado que executa um servidor Web e responde a solicitações de invocação de função do host do Functions.

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
)

type InvokeRequest struct {
	Data     map[string]json.RawMessage
	Metadata map[string]interface{}
}

type InvokeResponse struct {
	Outputs     map[string]interface{}
	Logs        []string
	ReturnValue interface{}
}

func orderHandler(w http.ResponseWriter, r *http.Request) {
	var invokeRequest InvokeRequest

	d := json.NewDecoder(r.Body)
	d.Decode(&invokeRequest)

	var reqData map[string]interface{}
	json.Unmarshal(invokeRequest.Data["req"], &reqData)

	outputs := make(map[string]interface{})
	outputs["message"] = reqData["Body"]

	resData := make(map[string]interface{})
	resData["body"] = "Order enqueued"
	outputs["res"] = resData
	invokeResponse := InvokeResponse{outputs, nil, nil}

	responseJson, _ := json.Marshal(invokeResponse)

	w.Header().Set("Content-Type", "application/json")
	w.Write(responseJson)
}

func main() {
	customHandlerPort, exists := os.LookupEnv("FUNCTIONS_CUSTOMHANDLER_PORT")
	if !exists {
		customHandlerPort = "8080"
	}
	mux := http.NewServeMux()
	mux.HandleFunc("/order", orderHandler)
	fmt.Println("Go server Listening on: ", customHandlerPort)
	log.Fatal(http.ListenAndServe(":"+customHandlerPort, mux))
}

Nesse exemplo, o manipulador personalizado executa um servidor Web para manipular eventos HTTP e é definido para escutar solicitações por meio de FUNCTIONS_CUSTOMHANDLER_PORT.

Embora o host do Functions receba a solicitação HTTP original em /api/order, ele invoca o manipulador personalizado usando o nome da função (seu nome de pasta). Nesse exemplo, a função é definida no caminho de /order. O host envia ao manipulador personalizado uma solicitação HTTP no caminho de /order.

À medida que as solicitações POST são enviadas para essa função, os dados de gatilho e os metadados de função ficam disponíveis por meio do corpo da solicitação HTTP. O corpo da solicitação HTTP original pode ser acessado no Data.req.Body do payload.

A resposta da função é formatada em pares chave/valor em que o membro Outputs mantém um valor JSON em que as chaves correspondem às saídas, conforme definido no arquivo function.json.

Esse é um exemplo de payload que esse manipulador retorna para o host do Functions.

{
  "Outputs": {
    "message": "{\"id\":1005,\"quantity\":2,\"color\":\"black\"}",
    "res": {
      "body": "Order enqueued"
    }
  },
  "Logs": null,
  "ReturnValue": null
}

Ao definir a saída message igual aos dados de pedidos que vieram da solicitação, a função gera esses dados de pedidos para a fila configurada. O host do Functions também retorna a resposta HTTP configurada em res para o chamador.

Função somente HTTP

Para funções disparadas por HTTP sem associações ou saídas adicionais, talvez você queira que o manipulador trabalhe diretamente com a solicitação e resposta HTTP em vez de conteúdo dos payloads de solicitação e resposta do manipulador personalizado. Esse comportamento pode ser configurado no host.json usando a configuração enableForwardingHttpRequest.

Importante

A principal finalidade do recurso de manipuladores personalizados é habilitar linguagens e runtimes que atualmente não têm suporte de primeira classe no Azure Functions. Embora possa ser possível executar aplicativos Web usando manipuladores personalizados, o Azure Functions não é um proxy reverso padrão. Alguns recursos, como transmissão de resposta, HTTP/2 e WebSockets, não estão disponíveis. Alguns componentes da solicitação HTTP, como determinados cabeçalhos e rotas, podem estar restritos. Seu aplicativo também pode experimentar a inicialização a frio excessiva.

Para tratar dessas circunstâncias, cogite executar seus aplicativos Web no Serviço de Aplicativo do Azure.

O exemplo a seguir demonstra como configurar uma função disparada por HTTP sem associações ou saídas adicionais. O cenário implementado nesse exemplo apresenta uma função chamada hello que aceita um GET ou POST.

Implementação

Em uma pasta chamada hello, o arquivo function.json configura a função disparada por HTTP.

hello/function.json

{
  "bindings": [
    {
      "type": "httpTrigger",
      "authLevel": "anonymous",
      "direction": "in",
      "name": "req",
      "methods": ["get", "post"]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}

A função é configurada para aceitar as solicitações GET e POST, e o valor do resultado é fornecido por meio de um argumento chamado res.

Na raiz do aplicativo, o arquivo host.json é configurado para executar handler.exe, e enableForwardingHttpRequest é definida como true.

{
  "version": "2.0",
  "customHandler": {
    "description": {
      "defaultExecutablePath": "handler.exe"
    },
    "enableForwardingHttpRequest": true
  }
}

Quando enableForwardingHttpRequest for true, o comportamento das funções somente HTTP difere do comportamento padrão dos manipuladores personalizados das seguintes maneiras:

  • A solicitação HTTP não contém o payload da solicitação dos manipuladores personalizados. Em vez disso, o host do Functions invoca o manipulador com uma cópia da solicitação HTTP original.
  • O host do Functions invoca o manipulador com o mesmo caminho da solicitação original, incluindo qualquer parâmetro de cadeia de caracteres de consulta.
  • O host do Functions retorna uma cópia da resposta HTTP do manipulador como a resposta à solicitação original.

A seguir, há uma solicitação POST para o host do Functions. O host do Functions envia uma cópia da solicitação para o manipulador personalizado no mesmo caminho.

POST http://127.0.0.1:7071/api/hello HTTP/1.1
Content-Type: application/json

{
  "message": "Hello World!"
}

O arquivo handler.go implementa um servidor Web e uma função HTTP.

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	if r.Method == "GET" {
		w.Write([]byte("hello world"))
	} else {
		body, _ := ioutil.ReadAll(r.Body)
		w.Write(body)
	}
}

func main() {
	customHandlerPort, exists := os.LookupEnv("FUNCTIONS_CUSTOMHANDLER_PORT")
	if !exists {
		customHandlerPort = "8080"
	}
	mux := http.NewServeMux()
	mux.HandleFunc("/api/hello", helloHandler)
	fmt.Println("Go server Listening on: ", customHandlerPort)
	log.Fatal(http.ListenAndServe(":"+customHandlerPort, mux))
}

Nesse exemplo, o manipulador personalizado cria um servidor Web para manipular eventos HTTP e é definido para escutar solicitações por meio de FUNCTIONS_CUSTOMHANDLER_PORT.

As solicitações GET são tratadas ao retornar uma cadeia de caracteres, e as solicitações POST têm acesso ao corpo da solicitação.

A rota para a função de pedido aqui é /api/hello, igual à solicitação original.

Observação

FUNCTIONS_CUSTOMHANDLER_PORT não é a porta voltada para o público usada para chamar a função. Essa porta é usada pelo host do Functions para chamar o manipulador personalizado.

Implantando

Um manipulador personalizado pode ser implantado em cada opção de hospedagem do Azure Functions. Se seu manipulador exigir dependências do sistema operacional ou da plataforma (como um runtime de linguagem), talvez seja necessário usar um contêiner personalizado.

Ao criar um aplicativo de funções no Azure para manipuladores personalizados, recomendamos que você selecione o .NET Core como a pilha.

Para implantar um aplicativo do manipulador personalizado usando Azure Functions Core Tools, execute o comando a seguir.

func azure functionapp publish $functionAppName

Observação

Verifique se todos os arquivos necessários para executar o manipulador personalizado estão na pasta e incluídos na implantação. Se seu manipulador personalizado for um executável binário ou tiver dependências específicas da plataforma, verifique se esses arquivos correspondem à plataforma de implantação de destino.

Restrições

  • O servidor Web do manipulador personalizado precisa começar dentro de 60 segundos.

Exemplos

Confira o repositório do GitHub de exemplos de manipulador personalizado para obter exemplos de como implementar funções em uma variedade de linguagens diferentes.

Solução de problemas e suporte

Log de rastreamento

Caso o processo do manipulador personalizado falhe ao iniciar ou caso ele tenha problemas de comunicação com o host do Functions, você poderá aumentar o nível de log do aplicativo de funções para Trace para ver mais mensagens de diagnóstico do host.

Para alterar o nível de log padrão do aplicativo de funções, defina a configuração logLevel na seção logging do host.json.

{
  "version": "2.0",
  "customHandler": {
    "description": {
      "defaultExecutablePath": "handler.exe"
    }
  },
  "logging": {
    "logLevel": {
      "default": "Trace"
    }
  }
}

O host do Functions gera mensagens de log extras, incluindo informações relacionadas ao processo do manipulador personalizado. Use os logs para investigar problemas que iniciam o processo do manipulador personalizado ou que invocam funções em seu manipulador personalizado.

Localmente, os logs são impressos no console.

No Azure, confira os rastreamentos do Application Insights para exibir as mensagens de log. Caso seu aplicativo produza um alto volume de logs, apenas um subconjunto de mensagens de log será enviado para o Application Insights. Desabilite a amostragem para garantir que todas as mensagens sejam registradas em log.

Testar o manipulador personalizado em isolamento

Os aplicativos do manipulador personalizado são um processo de servidor Web, portanto, pode ser útil iniciá-lo por conta própria e testar invocações de função enviando solicitações HTTP fictícias usando uma ferramenta como cURL ou Postman.

Você também pode usar essa estratégia em seus pipelines de CI/CD para executar testes automatizados em seu manipulador personalizado.

Ambiente de execução

Os manipuladores personalizados são executados no mesmo ambiente de um aplicativo Azure Functions típico. Teste seu manipulador para garantir que o ambiente contenha todas as dependências necessárias para ser executado. Para aplicativos que exijam dependências adicionais, talvez seja necessário executá-los usando uma imagem de contêiner personalizada hospedada no plano Premium do Azure Functions.

Obter suporte

Caso precise de ajuda em um aplicativo de funções com manipuladores personalizados, você poderá enviar uma solicitação por meio de canais de suporte regular. Porém, devido à grande variedade de linguagens possíveis usadas para criar aplicativos de manipuladores personalizados, o suporte não é ilimitado.

O suporte estará disponível caso o host do Functions tenha problemas para iniciar ou se comunicar com o processo do manipulador personalizado. Para problemas específicos do funcionamento interno de seu processo do manipulador personalizado, como problemas com a linguagem ou estrutura escolhida, nossa equipe de suporte não pode fornecer assistência nesse contexto.

Próximas etapas

Comece a criar um aplicativo do Azure Functions em Go ou Rust com o guia de início rápido dos manipuladores personalizados.