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

oieduardorabelo

Eduardo Rabelo

Posted on February 1, 2020

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: SimpleWebService

plugins:
  - 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: ScalableWebhook

plugins:
  - 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!!


Créditos

💖 💪 🙅 🚩
oieduardorabelo
Eduardo Rabelo

Posted on February 1, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related