O que você vai ver nesse artigo
- AWS services: APIGateway, Lambda, DynamoDB
- LocalStack Community
- NodeJs
- Docker Compose
- Terraform
O que vamos fazer
Definição da nossa POC (Proof of Concept)
O objetivo desse artigo é exemplificar o uso da LocalStack e o que é necessário para rodar um projeto simples. Para além do que usamos aqui, a ferramenta possui inúmeros outros recursos, bem como outros serviços ofertados pela AWS, que não abordarei.
Nossa POC é portanto muito simples. Trata-se de uma solução que envolve um APIGateway que recebe requisições para busca, listagem e criação de mensagens de vilões famosos. As requests de criação são tratadas por um Lambda criado com NodeJs que grava essas mensagens em uma fila SQS que por sua vez tem um gatilho para que outro Lambda recupere essa frase e a persista em um banco DynamoDB. Na parte de listagem e detalhamento da mensagem, usamos outro Lambda que busca a mensagem da base de dados e retorna para o usuário.
Toda a configuração da aplicação foi feita usando o Terraform com arquivos de configuração bastante simples de entender.
Repositório
Todo esse projeto e os códigos completos estão disponíveis no seguinte repositório. Este texto contém alguns
pedaços de códigos que podem não estar completos.
TL;DR
Para quem quer um passo-a-passo rápido de como verificar e executar essa POC, continue nessa sessão. Se você prefere uma explicação bastante minuciosa das decisões e formas para alcançar o resultado do projeto, pule para a sessão seguinte.
Preparando o ambiente:
- Clone o projeto do repositório;
- Instale o Docker a partir da documentação;
- Instale o docker-compose:
sudo curl -L "https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
- Instale o Python seguindo a documentação oficial;
- Instale o AWS CLI: página oficial;
- Instale o Localstack:
sudo python3 -m pip install localstack
- Instale o awscli-local:
pip install awscli-local
- Siga a orientação oficial para instalar o Terraform;
- NodeJs:
- NVM: repositório oficial
- NodeJs: comando descrito no repositório
Rodando a aplicação:
- No diretório raiz execute o comando:
docker-compose up -d
- No diretório terraform execute:
terraform init
terraform apply --auto-approve
- Para testar:
- Instale a extensão REST Client para o VSCode;
- Abra o arquivo
test.http
- Altere a variável
API_ID
para o id informado na saída do terraform; - Clique em send request para envio da request de POST;
- Verifique os serviços e logs da sua aplicação em https://app.localstack.cloud/;
- Verifique, após alguns segundos, se a frase está salva na base de dados, enviando a request de GET com o seu vilão favorito.
Matando a aplicação:
- Use o comando
terraform destroy --auto-approve
a partir do diretório terraform para remover os serviços AWS - Do diretório raiz, utilize o comando
docker-compose down -v
para apagar todos os recursos criados através do docker-compose.
Versão estendida: tudo o que você precisa para criar a sua própria POC
Na sessão anterior, fizemos um passo-a-passo rápido para rodar a aplicação. Nessa sessão vamos ir mais a fundo, e explicar tudo o que for necessário para a criação da sua própria POC, do zero.
Configurando o ambiente
Estou usando uma máquina rodando Ubuntu 20.04.3 LTS. Tudo o que for feito aqui também é possível usando uma máquina Windows ou Mac. Porém, os métodos para instalação mudam.
Docker
Vamos começar instalando o Docker. Segui precisamente a documentação da página oficial para instalação no Ubuntu. Você pode verificar a versão instalada no seu ambiente com o comando docker -v
. Para mim, ele retorna: Docker version 20.10.12, build e91ed57.
Docker Compose
Docker Compose foi um pouco mais complicado para instalar. A página do Docker aponta para uma versão muito antiga do compose. Preferi entrar no repositório do github para verificar e alterar o endereço no comando fornecido na página do Docker. Desta forma, executei os seguintes comandos:
sudo curl -L "https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
Agora, a versão que tenho do docker-compose instalada é:
leandro@leandro-desktop ~> docker-compose -v
Docker Compose version v2.2.3
Python
A LocalStack usa o compilador Python. Assim precisei instalá-lo, além do gerenciador de pacotes. Segui a documentação oficial. Instalei as seguintes versões:
leandro@leandro-desktop:~$ python3 --version
Python 3.8.10
leandro@leandro-desktop:~$ pip --version
pip 20.0.2 from /usr/lib/python3/dist-packages/pip (python 3.8)
AWS CLI
Para rodar comandos no terminal para acesso aos dados de serviços da AWS, mesmo que na LocalStack, é necessário usar o AWS CLI. Para sua instalação, eu segui o passo a passo informado na página oficial. Verificando a versão, obtive:
leandro@leandro-desktop ~> aws --version
aws-cli/2.4.11 Python/3.8.8 Linux/5.4.0-94-generic exe/x86_64.ubuntu.20 prompt/off
LocalStack
Para instalar o LocalStack eu tentei seguir o repositório oficial que dizia que não deveria ser instalado usando sudo, mas não funcionou. Instalei com o seguinte comando:
sudo python3 -m pip install localstack
e a versão que tenho instalada é:
leandro@leandro-desktop:~$ LocalStack --version
0.13.3.1
Também instalei o awscli-local com o comando pip install awscli-local
.
Terraform
O Terraform é uma ferramenta de código de infraestrutura ou infrastructure as a code. É open source e mantido pela HashCorp. Para instalar, segui a orientação oficial. Após instalado, testei a versão para:
leandro@leandro-desktop:~$ terraform -v
Terraform v1.1.3
on linux_amd64
NodeJs
A escolha de NodeJs para esse tutorial foi bastante concorrida com Python. Mas pesou o conhecimento e a reutilização do JavaScript em front-end. Aqui vai uma análise bem interessante sobre as possíveis linguagens para desenvolvimento para AWS Lambdas.
Para instalar o Node no meu ambiente eu optei por usar o NVM, gerenciador de versão do NodeJs. Esse gerenciador pode ser baixado do repositório oficial. Após instalado o NVM, basta seguir o comando descrito no repositório.
Com isso a versão do Node na minha máquina ficou:
leandro@leandro-desktop:~$ node -v
v16.13.2
leandro@leandro-desktop:~$ npm -v
8.1.2
AWS Cloud
Escolhi abordar os seguintes serviços da AWS:
- API Gateway: Permite criar endpoints e associá-los a um back-end.
- Cloudwatch: Permite monitorar a aplicação com alarmes e logs.
- Lambda: Permite executar uma função sem a necessidade de provisionar ou gerenciar um servidor.
- DynamoDB: Banco de dados não relacional, NoSQL, da AWS.
- SQS - Simple Queue Service: Como o nome bem informa, é um serviço de filas de mensagens.
As escolhas são baseadas em necessidades pessoais e naquilo que a LocalStack oferece gratuitamente.
LocalStack
Para iniciar nosso projeto, é necessário subir a LocalStack em um container com as configurações adequadas para o ele. Para isso usei o docker-compose e criei um script seguindo a página oficial. Além disso, busquei compreender e alterar alguns pontos, conforme a documentação de configuração. Com isso, meu docker-compose
ficou assim:
version: "3.8"
services:
localstack:
# Determino o nome do container principal da aplicação.
container_name: ${LOCALSTACK_DOCKER_NAME-localstack-terraform-tutorial}
image: localstack/localstack
network_mode: bridge
ports:
- "127.0.0.1:4566:4566"
- "127.0.0.1:4571:4571"
environment:
# Como eu resolvi trocar o nome do container principal eu preciso informar em uma
# variável de ambiente.
- MAIN_CONTAINER_NAME=${LOCALSTACK_DOCKER_NAME-localstack-terraform-tutorial}
# Informo os serviços da AWS que desejo usar.
- SERVICES=${SERVICES-dynamodb,lambda,apigateway,sqs,cloudwatch}
# Diretório para salvar dados localmente.
- DATA_DIR=${DATA_DIR-tmp/localstack/data}
# Como nossas funções lambda serão executadas. Nesse caso escolho o padrão
# rodar as funções dentro de containers para cada uma.
- LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR-docker}
# Esse parâmetro diz respeito a como a função será passada para o container.
# Se ela será montada no container ou será copiada. Nesse caso, escolhi copiar
# todo o arquivo zip para dentro do container.
- LAMBDA_REMOTE_DOCKER=true
volumes:
- "${TMPDIR:-/tmp}/localstack:/tmp/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
Para rodar o docker-compose, utilizei o comando docker-compose up
, ele vai subir todo o ambiente. Se quiser continuar a usar o mesmo terminal para outras coisas, adicione o -d
de detach. Para finalizar e se desfazer de todo o ambiente, basta rodar o docker-compose down -v
. O -v
informa que você também quer que os volumes criados sejam excluídos, liberando todos os recursos do computador.
Uma vez em execução, é possível verificar se está tudo funcionando corretamente através da URL http://localhost:4566/health e do dashboard oferecido pela LocalStack.
Terraform
Agora vamos prover os serviços e suas configurações através do Terraform, especificando os recursos em arquivos .tf
que coloquei na pasta terraform
.
Seguindo a documentação da LocalStack, primeiro declaramos o provider "aws"
:
provider "aws" {
access_key = "test"
secret_key = "test"
region = "us-east-1"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
endpoints {
apigateway = var.default_endpoint
cloudwatch = var.default_endpoint
dynamodb = var.default_endpoint
iam = var.default_endpoint
lambda = var.default_endpoint
sqs = var.default_endpoint
}
}
Observe que é necessário apontar os serviços da AWS para o LocalStack. Aqui, eu preferi criar uma variável default_endpoint
para manter o endereço:
variable "default_endpoint" {
description = "Endpoint padrão para os serviços AWS local."
default = "http://localhost:4566"
type = string
}
API
A declaração da api, o recurso quotes e os métodos são bem fáceis de compreender. E tem duas formas de se fazer isso. A primeira, é declarar blocos para cada api, recurso, integração, método:
# Declarando nossa api para acesso de frases e os métodos
resource "aws_api_gateway_rest_api" "quotes" {
name = "Quotes"
description = "Api para consumo e envio de frases para a aplicação."
}
resource "aws_api_gateway_resource" "quotes" {
rest_api_id = aws_api_gateway_rest_api.quotes.id
parent_id = aws_api_gateway_rest_api.quotes.root_resource_id
path_part = "quotes"
}
resource "aws_api_gateway_method" "get_quotes" {
rest_api_id = aws_api_gateway_rest_api.quotes.id
resource_id = aws_api_gateway_resource.quotes.id
http_method = "GET"
authorization = "NONE"
}
resource "aws_api_gateway_method" "post_quote" {
rest_api_id = aws_api_gateway_rest_api.quotes.id
resource_id = aws_api_gateway_resource.quotes.id
http_method = "POST"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "quote_receiver" {
rest_api_id = aws_api_gateway_rest_api.quotes.id
resource_id = aws_api_gateway_resource.quotes.id
http_method = aws_api_gateway_method.post_quote.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.quote_receiver.invoke_arn
}
resource "aws_api_gateway_deployment" "quotes" {
rest_api_id = aws_api_gateway_rest_api.quotes.id
stage_name = "dev"
}
Uma forma mais interessante de fazer isso é usando o OpenAPI para declarar nossa api. Com isso, o nosso arquivo rest-api.tf
fica bem mais simples:
resource "aws_api_gateway_rest_api" "quotes" {
name = "Quotes API"
body = templatefile("./openapi.json",
{
quote_receiver = "${aws_lambda_function.quote_receiver.invoke_arn}",
quote_recover = "${aws_lambda_function.quote_recover.invoke_arn}"
}
)
}
resource "aws_api_gateway_deployment" "quotes" {
rest_api_id = aws_api_gateway_rest_api.quotes.id
}
resource "aws_api_gateway_stage" "quotes" {
deployment_id = aws_api_gateway_deployment.quotes.id
rest_api_id = aws_api_gateway_rest_api.quotes.id
stage_name = "dev"
}
Eu prefiro declarar o OpenApi usando yaml, mas, por alguma razão, o Terraform não aceita yaml na sua definição do body. Por isso, instalei a extensão openapi-designer que compila os arquivos yaml para um único arquivo json. Minha definição para a API ficou assim:
openapi: 3.0.3
info:
title: Quotes Api
description: Api para consumo e envio de frases para a aplicação.
version: "1.0"
paths:
/quotes:
post:
summary: Permite gravar uma nova frase vilanesca!
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Quote"
x-amazon-apigateway-auth:
type: none
x-amazon-apigateway-integration:
uri: ${quote_receiver}
httpMethod: POST
type: AWS_PROXY
responses:
"201":
description: Frase vilanesca gravada com sucesso!
get:
summary: Retorna as frases vilanesca de um vilão.
parameters:
- name: author
in: query
required: true
description: O grade vilão por trás das frases.
schema:
type: string
x-amazon-apigateway-auth:
type: none
x-amazon-apigateway-integration:
uri: ${quote_recover}
httpMethod: POST
type: AWS_PROXY
responses:
"200":
description: As frases vilanescas para o vilão selecionado.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Quote"
Nossa API, portanto, tem um recurso quote
que é disponível no path de /quotes
e permite os métodos POST
e GET
sem necessidade de qualquer autorização de acesso.
Como vimos em nosso diagrama, o objetivo do projeto é que as frases enviadas sejam enviadas para uma fila por uma função Lambda e depois recuperada por outra função e gravada no banco de dados. Aqui, já declaramos a integração também com a função lambda. Vale notar:
- Observe que a integração agora é passada como parte do método através da propriedade
x-amazon-apigateway-integration
. O mesmo para a authorization com a propriedadex-amazon-apigateway-auth
. - O Terraform trata o arquivo pelo método templatefile que troca os valores como
${quote_receiver}
pelo valor passado como parâmetro. -
integration_http_method
tem que ser do tipoPOST
para integração com Lambda. Ele informa como a api vai interagir com o backend; -
type
deve ser, no nosso caso,AWS_PROXY
. Isso permite que a integração chame um recurso da AWS, no nosso caso a Lambda, e passe a request para a Lambda tratar. - No arquivo yaml acima, observe que falta a parte de
components
, que pode ser encontrada no repositório.
Lambdas
Para receber a mensagem da API, declaramos no nosso lambda.tf
:
# Lambdas para processar as frases
data "archive_file" "quote_receiver" {
type = "zip"
output_path = "../lambdas/dist/quote_receiver.zip"
source_dir = "../lambdas/quote-receiver/"
}
resource "aws_lambda_function" "quote_receiver" {
function_name = "quote_receiver"
filename = data.archive_file.quote_receiver.output_path
source_code_hash = data.archive_file.quote_receiver.output_base64sha256
handler = "index.handler"
runtime = "nodejs14.x"
role = "fake_role"
environment {
variables = {
SQS_URL = "${resource.aws_sqs_queue.quotes.url}"
}
}
}
---
resource "aws_lambda_event_source_mapping" "quotes" {
event_source_arn = aws_sqs_queue.quotes.arn
function_name = aws_lambda_function.quote_persister.arn
}
Geramos aqui um arquivo que foi criado na execução do terraform a partir da compactação dos dados dentro da pasta lambdas/quote-receiver
. O arquivo compactado é referenciado na criação da função. O mesmo ocorre para nossas outras funções, de persistência dos dados e de recuperação.
Como é possível notar, temos um mapeamento da função quote_persister
com a fila SQS
. Isso nos permite receber eventos com as informações inseridas na fila sempre que forem adicionadas novas mensagens à ela.
Um ponto interessante é que é possível passar variáveis de ambiente para a função, como é visto na passagem da variável SQS_URL
.
Fila SQS
Declarar a fila também é muito simples:
resource "aws_sqs_queue" "quotes" {
name = "Quotes"
}
DynamoDB
O provisionamento de uma tabela nova no dynamo demanda apenas os campos a seguir:
resource "aws_dynamodb_table" "quotes" {
name = "Quotes"
hash_key = "author"
billing_mode = "PAY_PER_REQUEST"
attribute {
name = "author"
type = "S"
}
}
Poderíamos informar os demais atributos, mas apenas hash_key
é obrigatória.
Esse atributo é equivalente para a AWS ao partition key
. Caso eu desejasse criar uma sort key
eu deveria passá-la como
range_key
e também informar os dados do atributo. No código exemplo usei sort key
para me permitir usar frases diferentes
do mesmo autor.
Código
Temos apenas três funções muito simples escritas usando NodeJs: uma para receber quotes, uma para persistir e outra para recuperar. Os códigos completos estão no repositório, mas
vale a pena pontuar alguns detalhes:
const { DynamoDBClient, BatchWriteItemCommand } = require("@aws-sdk/client-dynamodb");
const { Marshaller } = require("@aws/dynamodb-auto-marshaller");
const client = new DynamoDBClient({ endpoint: `http://${process.env.LOCALSTACK_HOSTNAME}:4566` });
const marshaller = new Marshaller();
exports.save = (quotes) => client.send(new BatchWriteItemCommand({
RequestItems: {
"Quotes": quotes
.map(quote => marshaller.marshallItem(quote))
.map(item => new Object({ PutRequest: { Item: item } }))
}
}));
- O código para persistência de dados no Dynamo mostra o uso da SDK para JavaScript V3.
- Diferente da V2, essa versão permite importar apenas módulos que realmente serão necessários para a aplicação, deixando a Lambda muito mais leve.
- É necessário configurar o endpoint para os serviços da AWS.
- Usei a biblioteca
Marshaller
que é um mapper entre valores nativos JavaScript e AttributeValues do DynamoDB. - A melhor forma de ver logs da aplicação é pelo dashboard da LocalStack.
Rodando nossa aplicação
Uma vez que o ambiente está rodando através do docker-compose, podemos entrar na pasta terraform
e executar o comando terraform init
. Este comando irá criar uma pasta .terraform
e outros arquivos no projeto. Quando terminado é a vez do comando terraform apply --auto-approve
que realmente provisiona todos os recurso que declaramos no nossos arquivos .tf
. Ao final, o comando irá disponibilizar como retorno a API_ID
, necessária para testarmos a api em um http client.
Assim que a aplicação subir, podemos testar sua funcionalidade com a collection de requests disponível no arquivo test.http
. Para rodar os testes nesse arquivo é necessário ter instalada a extensão REST Client para o Visual Studio Code. Uma vez instalada, troque o valor da variável @API_ID para aquele retornado pelo terraform e clique em send request.
Para encerrar nossa aplicação, basta rodar o comando docker-compose down -v
a partir do diretório raiz do projeto.
Conclusão
A LocalStack é uma ótima ferramenta para rodar a aplicação em ambiente local, durante o desenvolvimento. Ela elimina a preocupação de
se sujar um ambiente compartilhado por mais desenvolvedores ou de se acabar tendo gastos financeiros por uso indevido da AWS, melhorando muito o processo de criação do código. Essas funcionalidades ficam ainda mais
interessantes quando se é um iniciante em Cloud por que não há riscos de se acabar pagando por algo que estava nas entrelinhas.
Um aspecto que não foi abordado mas vale a pena pesquisar são as bibliotecas e integração da LocalStack com testes de integração. É possível rodar testes automatizados sem onerar a cloud.
Top comments (0)