Escrever todo o código lógico do seu domínio de negócios na função de entrada do manipulador Lambda pode ser muito tentador. Ouvi muitas desculpas durante a revisão de código: "São apenas algumas dezenas de linhas de código" ou "Ainda é muito legível".
Mas sejamos realistas: quando você desenvolveu serviços regulares que não fossem sem servidor, você não escreveu todo o código do serviço em um arquivo ou função.
Você modelou seu código em classes e módulos e atribuiu a cada um uma única responsabilidade com base nos princípios SOLID .
Escrever funções Lambda não deve ser diferente. Sua função handler não deve conter a lógica de domínio de negócios real, nem deve acessar sua tabela DynamoDB. E há boas razões para isso que abordaremos neste post.
Então, neste post do blog, você aprenderá como escrever código de função do AWS Lambda contendo três camadas arquitetônicas: o manipulador, a lógica e a camada de acesso a dados. Essas camadas resultam em um código bem organizado que é mais fácil de manter, mais fácil de testar e, como resultado, leva a menos bugs no futuro. Além disso, discutiremos o tratamento de erros.
Um projeto de serviço Python Serverless complementar que implementa esses conceitos pode ser encontrado aqui .
Índice
Camadas de arquitetura da função AWS Lambda
As camadas de arquitetura do AWS Lambda são os blocos de construção de aplicativos sem servidor que permitem que os desenvolvedores escrevam códigos reutilizáveis, organizados e fáceis de manter com uma única responsabilidade.
Esses princípios combinam bem com os princípios SOLID, onde o "S" significa responsabilidade única.
Encapsular um único código de responsabilidade em uma única camada reduz a síndrome do código espaguete . Por exemplo, imagine um caso de uso em que chamadas de API para seu banco de dados estão espalhadas por arquivos e funções não relacionados. Agora imagine um cenário melhor em que todas as chamadas de API para seu banco de dados residem em um único módulo ou arquivo, uma única camada. A única responsabilidade dessa camada é manipular conexões e chamadas de API para o banco de dados. Esse encapsulamento e responsabilidade única fazem com que:
Fácil de compartilhar o código entre várias funções Lambda, ou seja, chamá-lo de funções diferentes - zero duplicação de código.
Teste o código de responsabilidade única, ou seja, teste todas as APIs relacionadas ao banco de dados e casos extremos em um único módulo.
Mais fácil de manter o código. Quando você quer alterar uma chamada de API ou adicionar um cache, você faz as alterações em um módulo ou arquivo em vez de vários arquivos espalhados com possível duplicação de código.
Acredito que existam três camadas nas funções do AWS Lambda:
A camada do manipulador
A camada lógica
A camada de acesso a dados (DAL)
Em Python, uma camada é um módulo reutilizável, ou seja, um conjunto de arquivos.
Tratamento de erros entre camadas
Existem dois métodos: levantar uma exceção ou retornar um None ou True/False para marcar um sucesso ou falha. Eu tentei o método de valor de retorno em um dos meus serviços, e ele fica bagunçado rapidamente. Há uma ótima discussão no stack overflow , e eu sugiro que você dê uma olhada. Em Python, exceções são a maneira Pythonic de marcar que algo deu errado, mas você deve garantir que capturou todas as exceções. Exceções param de processar rapidamente entre camadas.
No entanto, levantar exceções específicas de camada de uma camada para outra pode quebrar seu conceito de responsabilidade única. Por que as camadas lógicas ou manipuladoras devem estar familiarizadas com as exceções do DynamoDB? Não deveriam. É por isso que você deve usar exceções personalizadas que marcam seu tipo: erro interno do servidor, exceção de solicitação inválida, etc.
Como isso funciona na prática? Cada camada é responsável por capturar suas exceções específicas de camada, registrá-las e re-gerá-las como uma das exceções personalizadas relevantes com o stack trace.
Mas quem captura essas exceções personalizadas? Bem, a camada do manipulador as capturará, pois é a camada que sabe como transformar essas exceções em uma saída relevante para o chamador. Discutiremos isso em mais detalhes na seção da camada do manipulador.
Exemplo de serviço sem servidor
Vamos analisar nosso serviço de exemplo 'Order' e analisar pelo que cada camada é responsável.
O projeto de modelo que usaremos é um serviço simples de 'pedidos'.
Ele tem uma API GW que aciona uma função AWS Lambda no caminho POST /api/orders.
Ele armazena todos os pedidos em uma tabela do Amazon DynamoDB.
Ele também implanta e armazena configuração dinâmica e sinalizadores de recursos no AWS AppConfig.
Leia mais sobre isso aqui .
O código completo pode ser encontrado aqui .
A camada do manipulador
A camada do manipulador é a função de entrada chamada quando a função é invocada.
Tem várias responsabilidades:
Carregar e verificar a configuração das variáveis de ambiente
Inicialize utilitários globais como logger, tracer, manipulador de métricas, etc.
Manipular validação de entrada
Passe a entrada para a camada lógica para continuar a lidar com a solicitação
Retorna a saída da camada lógica de volta para o chamador
Como você pode ver, há uma segregação completa de responsabilidades entre o manipulador e outras camadas. O manipulador carrega a função, garante que ela esteja configurada corretamente e delega a entrada para a camada lógica que sabe o que fazer com as informações.
Então, quando ele recebe de volta a saída da camada lógica, ele a retorna de uma maneira que só ele conhece de volta para o chamador. Nenhuma outra camada criará a resposta do chamador. Em nosso exemplo, a função Lambda retorna uma resposta JSON para o chamador. Assim, apenas o manipulador criará o JSON e atribuirá o código de resposta HTTP correto. Essa é sua responsabilidade.
Tratamento de erros
A camada do manipulador capturará qualquer exceção gerada devido a configurações incorretas, erros de utilitário global e entrada inválida e retornará a resposta correta ao chamador. Se a camada do manipulador for acionada por um SQS, a camada do manipulador pode enviar a entrada inválida para uma fila de letras mortas ou gerar uma exceção para retornar a carga útil para a fila.
A camada do manipulador também é necessária para capturar quaisquer exceções globais geradas pela camada lógica ou de acesso a dados (veja a explicação na seção de tratamento de erros).
Vamos dar uma olhada no manipulador 'create order' do serviço 'Order'. A função Lambda 'create order' cria novos pedidos para clientes e os salva na tabela do DynamoDB.
Nas linhas 22 a 26, inicializamos nossos utilitários globais: validador de variáveis de ambiente, rastreador e utilitários de métricas para a invocação atual e definimos o ID de correlação do registrador.
Nas linhas 31-36, buscamos nossa configuração dinâmica do AWS AppConfig e analisamos a resposta para garantir que trabalhamos com a configuração correta. Qualquer falha fará com que uma resposta HTTP de erro interno do servidor retorne ao chamador.
Nas linhas 38-44, nós analisamos e validamos a entrada de acordo com nosso esquema de entrada. Qualquer erro fará com que um erro HTTP BAD Request retorne ao chamador (como deveria!).
Nas linhas 48-55, enviamos a entrada do cliente para a camada lógica. O manipulador não sabe como a entrada é manipulada; ele apenas a delega para a próxima camada. A camada lógica retorna um objeto 'CreateOrderOutput'.
Na linha 58, o manipulador retorna 'CreateOrderOutput' ao chamador como uma resposta HTTP OK 200 com um corpo JSON criado a partir do objeto 'CreateOrderOutput'.
A Camada Lógica
Como o próprio nome sugere, a camada lógica contém todo o código lógico do domínio de negócios, funções e módulos necessários para processar a solicitação. É onde a mágica acontece. A camada lógica pode e deve ser compartilhada por vários manipuladores Lambda.
No contexto do nosso serviço 'orders', a camada lógica pode ter funções Python como 'create order', 'get order', 'update order' e 'delete order'. Cada função representa um caso de uso de negócios encapsulado e contém as validações necessárias, verificações lógicas, sinalizadores de recursos e, finalmente, o código que o implementa.
As camadas lógicas recebem uma entrada de seus parâmetros necessários da camada de manipulador que a chama - principalmente uma mistura de configuração necessária (nome da tabela de variáveis de ambiente) e o parâmetro de entrada do cliente (em nosso caso de uso: o número de produtos a serem comprados e o nome do cliente).
A camada lógica manipula a solicitação, chama a camada de acesso a dados (se necessário) e retorna um objeto de saída para a camada do manipulador. A camada lógica NÃO acessa nenhum banco de dados diretamente; ela sempre delega a ação necessária por meio de interfaces definidas para a camada de acesso a dados.
Relação com a camada de acesso a dados
A camada lógica é a única camada que chama a camada de acesso a dados. Como tal, ela está familiarizada com sua implementação de interface concreta; em nosso caso, ela inicializará um manipulador de camada de acesso a dados do DynamoDB e fornecerá ao seu construtor o nome da tabela do DynamoDB chamando uma função getter que retorna um objeto que implementa a interface DAL.
A camada lógica é a única camada familiar com o esquema de saída do manipulador e o esquema de entrada do banco de dados DAL. No nosso caso, criamos um novo pedido e retornamos o ID do pedido, o nome do cliente e a quantidade de produtos comprados. O ID do pedido é gerado na camada de acesso a dados, representando uma chave de tabela primária do DynamoDB. A camada lógica chamará a função de interface 'create_order_in_db' na camada DAL, obterá seu objeto de saída e o converterá no objeto de esquema de saída necessário.
Importante - não use o mesmo esquema para saída e entrada de banco de dados, fazendo assim um acoplamento entre eles. As camadas DAL e handler devem permanecer desacopladas, então você não precisará alterar sua API quando adicionar um campo ao esquema de entrada de banco de dados. Além disso, geralmente, a entrada de banco de dados contém metadados que pertencem à camada DAL, mas não devem ser retornados ao chamador da API REST. A função de conversão entre a entrada DAL e o esquema de saída filtrará os campos indesejados.
Vamos rever alguns exemplos de código:
Na linha 11, criamos um novo manipulador de camada DAL chamando uma função getter do manipulador DAL do módulo manipulador DAL do DynamoDB. A função é definida na camada DAL na implementação concreta do DynamoDB. A classe implementa as funções da interface DAL: 'create order', 'get order' etc.
Na linha 12, chamamos a função de interface DAL de função 'create_order_in_db' e salvamos o novo pedido. A camada lógica trabalha com um objeto que implementa a interface e não está familiarizada com nenhuma implementação interna além da função de inicialização (getter).
O ID do pedido é gerado na camada DAL. Neste exemplo, não há nenhuma lógica específica envolvendo a criação do pedido além de salvá-lo na tabela do DynamoDB.
Na linha 14, convertemos a entrada DAL para o esquema 'CreateOrderOutput'. Como você pode ver, já que é um exemplo simples, eles são idênticos; no entanto, como mencionado acima, em casos de uso mais avançados, apenas um subconjunto do esquema DAL é retornado ao chamador.
As definições de ambos os esquemas estão abaixo:
'OrderEntry' é definido na pasta schemas da camada DAL, pois representa uma entrada de banco de dados. Ele contém o id do pedido, o nome do cliente e o número de produtos pedidos.
'CreateOrderOutput' é definido na pasta de esquema dos manipuladores.
Camada de acesso a dados (DAL)
A camada de acesso a dados é a única camada que acessa a infraestrutura real do banco de dados, cria as conexões, está familiarizada com os detalhes de implementação do banco de dados e chama suas APIs.
A camada DAL apresenta uma interface para todas as ações de banco de dados necessárias pela camada lógica e um manipulador de banco de dados concreto que a implementa.
O tipo de banco de dados e a implementação são abstraídos pelo manipulador de banco de dados concreto que herda essa interface. O uso da interface representa outro princípio SOLID, inversão de dependência .
Nosso serviço 'order' tem uma interface de ações de banco de dados contendo uma função 'create_order_in_db' e em outra um arquivo na camada, uma classe de manipulador DAL do DynamoDB que implementa a interface.
Por que você deve usar uma interface e um manipulador de banco de dados que a implemente?
Esta interface simplifica a substituição do banco de dados no futuro. Tudo o que você precisa fazer é criar um novo manipulador que herde da interface. Claro, você precisará manipular a parte IaC (infraestrutura como código), criar os recursos e definir a função da função Lambda com as novas permissões, mas quanto ao código do aplicativo, tudo está encapsulado em uma nova classe de manipulador de banco de dados. Quando o novo manipulador estiver pronto, basta definir a camada lógica para criar uma nova instância do novo manipulador de banco de dados e usá-lo. Você pode até usar ambos como uma fase de teste até ganhar confiança suficiente de que está funcionando corretamente.
Vamos dar uma olhada no exemplo de código da interface:
Na linha 9, definimos o pedido criado na função do banco de dados. Cada implementação do banco de dados salvará o nome do cliente e a contagem de itens do pedido, mas com chamadas de API diferentes. Funções de interface futuras podem incluir obter um pedido por um id, atualizar um pedido e excluir um pedido.
Aqui está o exemplo de código completo para uma classe concreta que implementa a interface para um manipulador DAL do DynamoDB:
Na linha 16, herdamos a classe abstrata, implementando assim a interface em Python.
Na linha 28, implementamos a função de interface 'create_order_in_db'.
Na linha 29, geramos um ID de pedido que serve como chave primária do DynamoDB.
Na linha 32, criamos um objeto de entrada para inserir na tabela.
Na linha 34, criamos um objeto de tabela boto3 DynamoDB. Armazenamos o recurso em cache (nas linhas 22-25) por até 5 minutos para manter uma conexão ativa com a tabela entre invocações como uma otimização de desempenho.
Na linha 35, inserimos o novo pedido na tabela.
Nas linhas 37-39, lidamos com exceções. Conforme discutido antes , registramos a exceção e a levantamos novamente como uma exceção global - uma exceção de erro interno do servidor que a camada do manipulador capturará e manipulará.
Na linha 42, retornamos a entrada criada para a camada lógica.
Nas linhas 45-47, criamos um objeto manipulador Dal do DynamoDB concreto e usamos o decorador 'lru_cache' para torná-lo uma instância de classe singleton para que possa ser reutilizado em várias invocações. Este é o manipulador que a camada lógica usa. É a única implementação DAL concreta com a qual está familiarizada.
Comments