AWS Serverless: Padrões Serverless Implementados (Parte 1 de 2)

Eu acho que a melhor maneira de aprender algo é praticá-lo e tentar explicá-lo, então é isso que vou fazer na próxima série de artigos. Essas postagens serão baseadas no incrível artigo de Jeremy Daly sobre padrões serverless. Não vou copiar as palavras de Jeremy aqui; portanto, para cada padrão, vá ao artigo acima para ler sua descrição. Vou fornecer uma implementação técnica e mencionarei mais recursos que achei interessantes. Vamos começar!

Configuração Comum

Todos os projetos terão uma configuração comum, o que é bastante simples. Primeiro, inicialize um projeto Node.js:

yarn init

Em seguida, instale o Serverless Framework como uma dependência de desenvolvedor:

yarn add serverless --dev

E, finalmente, crie um script para implantar o projeto:

"scripts": {
"deploy": "serverless deploy --aws-profile serverless-local"
}

(Supondo que você tenha um perfil local chamado serverless-local)

Padrão 01 — O Serviço Web Simples

Você pode ler a explicação aqui.

Para implementar esse padrão, precisamos criar um serviço com uma tabela do DynamoDB e, pelo menos, uma função que obtenha ou defina dados dela. Então, a aparência do serverless.yml será assim:

service: SimpleWebServiceplugins:
- serverless-iam-roles-per-function
provider:
name: aws
runtime: nodejs10.x
region: ${opt:region, self:custom.defaultRegion}
custom:
defaultRegion: eu-west-1
tableName: ${self:provider.region}-SimpleWebServiceTable
functions:
GetItem:
handler: src/functions/getItem.handler
events:
- http:
method: get
path: item/{itemId}
environment:
tableName: ${self:custom.tableName}
iamRoleStatements:
- Effect: Allow
Action: dynamodb:getItem
Resource: !GetAtt SimpleWebServiceTable.Arn
PutItem:
handler: src/functions/putItem.handler
events:
- http:
method: post
path: item
environment:
tableName: ${self:custom.tableName}
iamRoleStatements:
- Effect: Allow
Action: dynamodb:putItem
Resource: !GetAtt SimpleWebServiceTable.Arn
resources:
Resources:
SimpleWebServiceTable:
Type: AWS::DynamoDB::Table
Properties:
KeySchema:
- AttributeName: id
KeyType: 'HASH'
AttributeDefinitions:
- AttributeName: id
AttributeType: 'N'
BillingMode: PAY_PER_REQUEST
TableName: ${self:custom.tableName}

Precisamos instalar o plugin serverless-iam-roles-per-function. Estamos criando uma tabela DyanamoDB e passando o nome para as funções via variável de ambiente. Em cada função, estamos apenas dando as permissões necessárias.

Vamos dar uma olhada na implementação da função PutItem:

const AWS = require("aws-sdk");
const dynamodb = new AWS.DynamoDB.DocumentClient();
const tableName = process.env.tableName;module.exports.handler = async (event) => {
const body = JSON.parse(event.body);
const id = parseInt(body.id);
const name = body.name;
const params = {
TableName: tableName,
Item: {
'id' : id,
'name' : name
}
};
const resp = await dynamodb.put(params).promise();
const res = {
statusCode: 200,
body: JSON.stringify(resp)
};
return res;
};

E, finalmente, vamos dar uma olhada na implementação da função GetItem:

const AWS = require("aws-sdk");
const dynamodb = new AWS.DynamoDB.DocumentClient();
const tableName = process.env.tableName;module.exports.handler = async (event) => {
const id = event.pathParameters.itemId;
const req = {
TableName: tableName,
Key: {
'id': parseInt(id)
}
};
const resp = await dynamodb.get(req).promise();
const res = {
statusCode: 200,
body: JSON.stringify(resp.Item)
};
return res;
};

Você pode conferir a solução completa aqui.

Padrão 02 — Webhook Escalável

Você pode ler a explicação aqui.

Nesse padrão, vamos introduzir uma fila SQS entre dois serviços e essa fila terá uma fila de mensagens não entregues caso encontremos algum erro. Então, vamos começar com o arquivo serverless.yml:

service: ScalableWebhookplugins:
- serverless-iam-roles-per-function

provider:
name: aws
runtime: nodejs10.x
region: ${opt:region, self:custom.defaultRegion}
custom:
defaultRegion: eu-west-1
functions:
Flooder:
handler: src/functions/flooder.handler
events:
- http:
method: post
path: flooder
environment:
queueUrl: !Ref WorkerQueue
iamRoleStatements:
- Effect: Allow
Action: SQS:SendMessage
Resource: !GetAtt WorkerQueue.Arn
Worker:
handler: src/functions/worker.handler
memorySize: 256
reservedConcurrency: 5
events:
- sqs:
batchSize: 10
arn: !GetAtt WorkerQueue.Arn
DLQReader:
handler: src/function/dlqReader.handler
events:
- sqs:
batchSize: 10
arn: !GetAtt ReceiverDeadLetterQueue.Arn
resources:
Resources:
WorkerQueue:
Type: "AWS::SQS::Queue"
Properties:
QueueName: "WorkerQueue"
VisibilityTimeout: 30 # 30 segundos
MessageRetentionPeriod: 60 # 60 segundos
RedrivePolicy:
deadLetterTargetArn: !GetAtt ReceiverDeadLetterQueue.Arn
maxReceiveCount: 3
ReceiverDeadLetterQueue:
Type: "AWS::SQS::Queue"
Properties:
QueueName: "WorkerDLQ"
MessageRetentionPeriod: 1209600 # 14 dias em segundos

Este arquivo é um pouco mais complicado que o anterior. As partes interessantes estão na definição da fila, onde estamos definindo as propriedades dela. Vamos explicar agora quais são esses parâmetros, mas eu recomendo fortemente que você leia este artigo de Jeremy sobre filas SQS e leia todos os comentários também, pois tem informações interessantes por lá. Você também pode dar uma olhada neste artigo, onde o autor explica como o tratamento de erros no SQS funciona. E, finalmente, você pode ir para a documentação oficial.

O tempo limite de visibilidade é o tempo em que a mensagem permanece na fila sem que outros consumidores possam receber e processar a mensagem, aguardando a confirmação (ou o erro) do consumidor original. Se, após esse período, a fila não receber a solicitação de exclusão do consumidor original, a fila disponibilizará a mensagem para o próximo consumidor.

O período de retenção de mensagens é o momento em que uma mensagem é colocada em uma fila antes de ser excluída pelo sistema, se ninguém a consumir. O máximo é 14 dias.

A política de redrive é onde especificamos o que acontece quando uma mensagem não pode ser processada pelo consumidor. No nosso caso, estamos dizendo que gostaríamos de especificar um DLQ (Dead Letter Queue) e que uma mensagem será enviada para o DLQ após três tentativas falhas de processamento.

O código de enviar mensagens é o mesmo (ou muito semelhante) que Jeremy tem em seu post sobre SQS:

const AWS = require('aws-sdk');
const SQS = new AWS.SQS();
module.exports.handler = async (event, context) => {
const body = JSON.parse(event.body);
const times = parseInt(body.times);
const queue = process.env.queueUrl;
console.log(`Queue is: ${queue}`);
for (let i=0; i<times; i++) {
await SQS.sendMessageBatch({ Entries: createMessages(), QueueUrl: queue }).promise()
}
return {
statusCode: 200,
body: JSON.stringify("all done")
};
}
const createMessages = () => {
let entries = []

for (let i=0; i<10; i++) {
entries.push({
Id: 'id'+parseInt(Math.random()*1000000),
MessageBody: 'value'+Math.random()
})
}
return entries
}

E o código do worker e do DLQReader são basicamente os mesmos:

let counter = 1
let messageCount = 0
let funcId = 'id'+parseInt(Math.random()*1000)

module.exports.handler = async (event) => {
counter++;
if (counter % 10 === 0){
throw new Error('Simulating error');
}

// Grava o número de mensagens recebidas
if (event.Records) {
messageCount += event.Records.length
}
console.log(funcId + ' REUSE: ', counter)
console.log(funcId + ' Message Count: ', messageCount)
console.log(JSON.stringify(event))
console.log(funcId + ' processing...');
await sleep(2000);
console.log(funcId + ' job done!');
return 'done'
};
const sleep = (milliseconds) => {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}

Você pode conferir a solução completa aqui.

Finalizando

Neste artigo, vimos a implementação de alguns padrões do excelente artigo de Jeremy Daly. Continuaremos com isso no artigo seguinte.

Espero que tenha te ajudado!!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store