Desenvolvendo funções AWS Lambda localmente com Serverless Offline
No primeiro post dessa série de artigos, passamos um pouco sobre o framework que utilizamos para fazer o gerenciamento das configurações e deploy de nossas APIs de coleta de dados baseadas em AWS Lambdas.
Nesse segundo post, vamos colocar a mão na massa para exemplificar como podemos desenvolver localmente o código dessas funções. Vamos lá!
Ferramentas:
Antes de começarmos a destrinchar o código, abaixo estão algumas informações gerais sobre as ferramentas utilizadas:
Docker: o docker é uma ferramenta bastante popular que consegue encapsular dentro de um contêiner tudo que sua aplicação necessita para funcionar. Um container é um ambiente isolado, que é similar a uma máquina virtual, mas difere por não ser um sistema operacional inteiro rodando dentro do seu localhost.
O tema “docker” é bastante extenso, se você quiser saber mais sobre ele, vou deixar aqui este artigo e o link da documentação oficial do docker.
Serverless Offline: esse plugin do Serverless inicia um servidor HTTP local que atua como ponto de entrada para o código da sua função. Ele trata todo o ciclo de vida das requisições da mesma maneira que um AWS API Gateway rodando no ambiente da AWS.
As configurações da função e do API Gateway emulados seguem o mesmo arquivo serverless.yml
que é utilizado para fazer o deploy da função Lambda, isso permite ter confiança de que as especificações de variáveis de ambiente, endpoints e runtime dessa versão local serão também utilizadas no ambiente da AWS.
Serverless Python Requirements: todo o código que é utilizado por uma função Lambda é enviado para a AWS no formato de zip
. Isso significa que todas as bibliotecas externas que são utilizadas pela sua aplicação devem estar contidas nesse zip
.
Para facilitar a criação desse pacote com os arquivos, o Serverless conta com o plugin Serverless Python Requirements, que automaticamente inspeciona o arquivo requirements.txt
(que usualmente é utilizado para definir as dependências externas a serem instaladas), e então coloca todos os arquivos dessas bibliotecas dentro de uma pasta que é incluída no zip final, logo antes do deploy.
Uma observação importante: não é necessária a utilização desse plugin para rodar o Serverless Offline, já que ele utiliza as bibliotecas instaladas no seu caminho padrão, mas achei que seria interessante incluir neste artigo como incluíriamos essas bibliotecas caso fôssemos fazer deploy de nossa função.
O que vamos construir:
Okay! Agora voltando para o código: a API a ser construída utiliza o framework Serverless em conjunto com o Serverless Offline para subir uma versão local de uma API python no formato de AWS Lambda. Essa função rodará dentro de um contêiner docker, e será acessível através de requisições HTTP POST apontando para localhost.
Para demonstrar o funcionamento do Serverless Python Requirements, o endpoint criado por essa API utiliza a biblioteca wikipedia, recebendo um tópico qualquer como entrada, e retornando o título das possíveis páginas da Wikipedia relacionadas a esse tópico. É uma funcionalidade bem aleatória, o propósito é exclusivamente mostrar como conseguir utilizar uma biblioteca externa na API.
Código:
Caso você queria seguir o tutorial, aqui está o repositório com os arquivos fonte:
Sem mais delongas, vamos começar com o código da nossa função Lambda:
import json
import wikipedia
def handler(event, context):
body = json.loads(event['body'])
print(f'Request body: {body}')
print(f'Requested endpoint: {event["path"]}') topic = body['topic']
return {
'statusCode': 200,
'body': json.dumps(
{'search_results': wikipedia.search(topic)}
)
}
A funçãohandler
é o ponto de entrada para qualquer endpoint da nossa API. Ela obrigatoriamente recebe como parâmetros os objetos event
e context
. O primeiro contém informações do request, como body, endpoint solicitado e headers. O segundo armazena informações referentes à função AWS Lambda em si, como por exemplo, nome e versão da função e informações do log group ao qual essa função pertence.
A funcionalidade implementada é bastante simples: o handler
acessa a chave topic
dentro do body da requisição, e então retorna um JSON
com uma única chave, cujo valor contém os resultados de wikipedia.search(topic)
Exemplo de entrada e saída:
topic = "Avengers"return {
"search_results": [
"Avenger",
"The Avengers (2012 film)",
"Avengers (comics)",
"Avengers: Endgame",
"Avengers: Infinity War",
"The Avengers (TV programme)",
"Marvel's Avengers (video game)",
"Avengers: Age of Ultron",
"List of Marvel Cinematic Universe films",
"Avengers Assemble (TV series)"
]
}
Uma observação: como criaremos um único endpoint, não precisamos fazer nenhum tipo de chaveamento para chamar funções subjacentes específicas por endpoint, mas caso quiséssemos, poderíamos obter o endpoint solicitado acessando event['path']
.
Arquivo de configuração:
Como mencionado anteriormente, tanto o Serverless quanto o Serverless Offline leem o arquivoserverless.yml
, onde se encontram as especificações da função e definições de quais arquivos contém o código fonte que deve ser empacotado no zip
. Abaixo vamos dar uma olhada em um exemplo desse arquivo:
A primeira seção do nosso serverless.yml
é a de plugins
. Nela estão listadas as extensões a serem utilizadas pelo Serverless.
Uma observação importante é de que essas extensões devem estar listadas no arquivo package.json
, e devem ser instaladas antes de poderem ser utilizadas.
service: offline-serverless-example
plugins:
- serverless-python-requirements
- serverless-offline
A segunda parte contém as informações relativas ao runtime da função: qual versão do python, em qual região da AWS, quanta memória será alocada para cada instância da sua função:
provider:
name: aws
runtime: python3.8
stage: ${opt:stage, 'local'}
region: sa-east-1
memorySize: 128
A terceira seção contém informações gerais da função, como nome e descrição, e é onde você deve indicar onde que está ohandler
que servirá como ponto de entrada, quais arquivos serão incluídos no zip
contendo os códigos fonte, e quais serão os endpoints disponíveis na sua API.
Handler: estamos indicando que a função a ser usada comohandler
encontra-se na pasta offline_serverless
, dentro do arquivo handler.py
e que o nome da função é handler
Package: para a criação do zip
, queremos incluir todos os arquivos com extensão *.py
dentro da pasta offline_serverless
. Note que, como listamos o serverless-python-requirement
na seção de plugins, não precisamos incluir nenhuma configuração em relação às bibliotecas adicionais que estarão presentes no requirements.txt
, já que esse plugin se encarregará disso por nós.
Endpoints: estamos criando um único /wikipedia_search
, que atenderá requisições do tipo POST
.
functions:
offline-serverless:
handler: offline_serverless/handler.handler
name: offline-serverless
description: Simple example on how to run serverless in offline mode.
package:
include:
- offline_serverless/*.py
events:
- http:
path: /wikipedia_search
method: post
Juntando tudo, temos a seguinte versão final do arquivo yml
:
service: offline-serverless-example
plugins:
- serverless-python-requirements
- serverless-offlineprovider:
name: aws
runtime: python3.8
stage: ${opt:stage, 'local'}
region: sa-east-1
memorySize: 128
functions:
offline-serverless:
handler: offline_serverless/handler.handler
name: offline-serverless
description: Simple example on how to run serverless in offline mode.
package:
include:
- offline_serverless/*.py
events:
- http:
path: /wikipedia_search
method: post
Com isso já conseguíriamos executar serverless deploy
para o deploy de nossa função, mas como o intuito é rodarmos uma versão local dela dentro de um contêiner, precisamos definir um Dockerfile com as configurações desse contêiner:
Dockerfile:
Iremos utilizar uma imagem base (pense nisso como se fosse a máquina virtual que vamos utilizar), em que já temos python e node instalados.
FROM nikolaik/python-nodejs:latest
O comando abaixo instalará o Serverless nesse contêiner.
RUN npm install -g serverless
Os comandos seguintes copiam para o caminho especificado na direita, os arquivos especificados na esquerda. Por exemplo, abaixo estamos copiando o arquivo ./package.json
para /app
, e o arquivo serverless.yml
para app/serverless.yml
.
O comando WORKDIR
é o equivalente a cd
, portanto estamos adentrando o diretório /app
para que os próximos comandos sejam executados de dentro dele.
COPY ./package.json /app/
COPY serverless.yml /app/serverless.yml
COPY offline_serverless/ /app/offline_serverless/
COPY requirements.txt /app/requirements.txt
WORKDIR /app/
Com esses arquivos copiados para a pasta /app
, os comandos abaixo irão instalar os plugins Serverless Offline e Serverless Python Requirements listados no package.json
e a biblioteca Wikipedia
listada em requirements.txt
RUN npm install
RUN pip install -r requirements.txt
Temos então a versão final do nosso Dockerfile
:
FROM nikolaik/python-nodejs:python3.8-nodejs15
RUN npm install -g serverless
RUN serverless --version
COPY ./package.json /app/
COPY serverless.yml /app/serverless.yml
COPY offline_serverless/ /app/offline_serverless/
COPY requirements.txt /app/requirements.txt
WORKDIR /app/
RUN npm install
RUN pip install -r requirements.txt
Executando o Serverless Offline:
Agora temos todos os arquivos para rodar nossa Lambda localmente!
Vamos construir nosso contêiner com o seguinte comando:
docker build -t serverless_example .
A flag -t corresponde a tag, e é usada para dar uma etiqueta ao nosso contêiner, nesse caso, serverless_example
.
O ponto .
indica que o Dockerfile
a ser utilizado se encontra dentro do diretório atual.
Provavelmente irão aparecer um monte de mensagens de warning. Não se preocupe, isso ocorre porque o Serverless não necessariamente utiliza todas as versões mais recentes de bibliotecas do npm.
A saída desse comando deve ser algo como:
Successfully built algum_id_aleatório
Successfully tagged serverless_example:latest
Agora vamos enviar um comando para rodar o Serverless Offline dentro desse contêiner:
docker run --network host serverless_example sls offline start
Não entrarei em muitos detalhes sobre como funciona networking de contêiners, mas basicamente, --network host
indica que queremos que o contêiner rode dentro da mesma rede que nossa máquina.
A parte serverless_example sls offline start
indica que queremos iniciar o serverless-offline
através do comando sls offline start
. Tudo isso dentro do contêiner serverless_example
.
Se tudo deu certo, você deve ver uma saída parecida com esta abaixo, indicando que a sua função está pronta para receber requisições.
offline: Function names exposed for local invocation by aws-sdk:
* offline-serverless: offline-serverless
POST | http://localhost:3000/local/wikipedia_search POST | http://localhost:3000/2015-03-31/functions/offline-serverless/invocations offline: [HTTP] server ready: http://localhost:3000 🚀
offline:
offline: Enter "rp" to replay the last request
Agora vamos testar nossa função com o comando abaixo:
curl --location --request POST 'http://localhost:3000/local/wikipedia_search' \
--header 'Content-Type: text/plain' \
--data-raw '{
"topic": "Game of Thrones"
}'
Você deve ver algo parecido com:
{
"search_results": [
"Game of Thrones",
"List of Game of Thrones episodes",
"A Game of Thrones",
"Game of Thrones (season 8)",
"Game of Thrones (season 5)",
"Game of Thrones (season 7)",
"The Iron Throne (Game of Thrones)",
"Game of Thrones (season 6)",
"List of Game of Thrones characters",
"Game of Thrones (season 1)"
]
}
E é isso! Consegumos rodar nossa função :)
Se quisermos testar quaisquer modificações, basta executar novamente os comandos para construir e rodar o contêiner.
Outro exemplo de uso do Serverless Offline:
Aqui na Legiti, usamos também o Serverless Offline para fazer testes de integração da nossa API de coleta de dados:
Antes de fazermos deploy da função no ambiente de staging, rodamos testes de integração no nosso pipeline automatizado de testes, onde é criado um contêiner rodando a API através do Serverless Offline, e um outro contêiner Postgres que replica nosso banco de produção. A nossa suíte de testes então envia requisições para a API, e em seguida checa se os dados foram inseridos corretamente no banco de dados.
Isso acelera bastante o processo de validação de mudanças a serem incluídas na API, dado que não há necessidade de realizar deploys para executar esses testes.
Voltando ao Serverless Python Requirements:
Antes de finalizar o artigo, gostaria de voltar rapidinho para mostrar o Serverless Python Requirements em ação; para isso vamos entrar no contêiner construído:
Rode docker run -it serverless_example sh
. Isso deve iniciar uma sessão interativa de bash
no seu terminal.
Em seguida, rode sls requirements install
. A saída deve ser algo como:
Serverless: Generated requirements from /app/requirements.txt in /app/.serverless/requirements.txt...Serverless: Installing requirements from /root/.cache/serverless-python-requirements/f9b65d6671485c70e32d4f73b2cb2828f85e87a0ea4f8e91e2b83d4c44659d61_slspyc/requirements.txt ...Serverless: Using download cache directory /root/.cache/serverless-python-requirements/downloadCacheslspyc
Serverless: Running ..
Uma pasta requirements
com todas as bibliotecas relacionadas à Wikipedia
deve ter sido criada em .serverless/requirements/
.
Para conferir, vamos rodar cd .serverless/requirements/ && ls -l
:
/app # cd .serverless/requirements/ && ls -l
total 72
drwxr-xr-x 2 root root 4096 Nov 22 21:22 beautifulsoup4-4.9.3.dist-info
drwxr-xr-x 2 root root 4096 Nov 22 21:22 bin
drwxr-xr-x 5 root root 4096 Nov 22 21:22 bs4
drwxr-xr-x 3 root root 4096 Nov 22 21:22 certifi
drwxr-xr-x 2 root root 4096 Nov 22 21:22 certifi-2020.11.8.dist-info
drwxr-xr-x 4 root root 4096 Nov 22 21:22 chardet
drwxr-xr-x 2 root root 4096 Nov 22 21:22 chardet-3.0.4.dist-info
drwxr-xr-x 3 root root 4096 Nov 22 21:22 idna
drwxr-xr-x 2 root root 4096 Nov 22 21:22 idna-2.10.dist-info
drwxr-xr-x 3 root root 4096 Nov 22 21:22 requests
drwxr-xr-x 2 root root 4096 Nov 22 21:22 requests-2.25.0.dist-info
-rw-r--r-- 1 root root 10 Nov 22 21:21 requirements.txt
drwxr-xr-x 3 root root 4096 Nov 22 21:22 soupsieve
drwxr-xr-x 2 root root 4096 Nov 22 21:22 soupsieve-2.0.1.dist-info
drwxr-xr-x 6 root root 4096 Nov 22 21:22 urllib3
drwxr-xr-x 2 root root 4096 Nov 22 21:22 urllib3-1.26.2.dist-info
drwxr-xr-x 3 root root 4096 Nov 22 21:22 wikipedia
drwxr-xr-x 2 root root 4096 Nov 22 21:22 wikipedia-1.4.0.dist-info
Vemos que todos os arquivos foram automaticamente incluídos dentro dessa pasta que, no momento do deploy, será incluída no zip
final juntamente com o código da sua aplicação.
Apesar dessa demonstração manual de como gerar essa pasta com as dependências, o serverless faz isso automaticamente, então não há necessidade de rodar esses comandos antes do deploy.
E assim chegamos ao fim desse artigo! Se surgirem quaisquer dúvidas, podem entrar em contato comigo :) No próximo eu pretendo falar um pouco sobre como configuramos uma AWS Lambda como um autorizador de requisições. Até lá!