Como criar uma aplicação serverless com mensageria utilizando HerbsJS e AWS SQS

Como criar uma aplicação serverless com mensageria utilizando HerbsJS e AWS SQS

Neste artigo, vamos abordar a criação de dois microsserviços serverless lambda conectados a uma fila de mensageria utilizando o HerbsJS de uma forma prática.

Mas primeiro, o que é mensageria? Mensageria é um conceito baseado na programação orientada a eventos, que consiste em intermediar a comunicação entre aplicações por meio de mensagens e é muito utilizado quando se deseja que os dados não sejam perdidos e no processamento de grande quantidade de dados.

E como funciona esse envio de mensagens? A mensagem é enviada através de um publicador para uma fila e um consumidor, que fica observando essa fila para que quando haja mensagens nela, ele já esteja apto a processar aquilo que foi enviado. Esse padrão de tramitação de mensagens se chama Pub/Sub (publisher/subscriber).

Existem várias aplicações que disponibilizam serviços de mensageria, esses  são chamados Message Brokers. Exemplos desse tipo de serviço são Apache Kafka, RabbitMQ, Memphis e o que vamos utilizar para nosso exemplo: o AWS SQS (Simple Queue Service).

O SQS tem dois tipos de fila: a fila padrão e a FIFO. Basicamente, a fila padrão trabalha liberando as mensagens na fila assincronamente e a FIFO libera por ordem de chegada (First In First Out).

Liberação de mensagens da fila padrão
Liberação de mensagens da fila FIFO

Ok, entendemos o que é uma fila, mas o que é esse negócio de serverless? Serverless,é uma arquitetura de computação orientada a eventos (o que conversa bem com mensageria) que, basicamente, permite a quem cria e mantém o software não se preocupar com a infraestrutura em que essas aplicações estão rodando.

Serverless, por padrão é auto escalável, ou seja, o provedor de nuvem contratado é responsável por aumentar ou diminuir o poder de processamento ou até a quantidade de instâncias de uma aplicação dado sua demanda e você só é cobrado pelo processamento que sua aplicação fez, não pelo tempo mantido "no ar".

Passados os conceitos, vamos por a mão na massa!

Vamos criar um Pub/Sub básico com uma fila padrão do SQS, abaixo vemos um desenho da nossa aplicação.

Arquitetura mensageria básica

E vamos implementar isso utilizando o HerbsJS, que é um framework focado em arquitetura limpa, que ajuda a gente no desenvolvimento focado nas regras de negócio. Ele tem colas que simplificam a camada de Infra (adapters) e te ajuda a desenvolver aplicações backend de maneira prática e ágil, adicionando mais valor ao seu domínio e lógica de negócio, sem perder tempo com camadas de infraestrutura e gastando seu tempo onde realmente importa, no domínio! Você pode aprender mais sobre o HerbsJS olhando sua documentação rica em detalhes e tutoriais aqui (aproveite e entre no nosso discord e bata um papo com a gente).

Acesso à AWS

Para conseguir criar os projetos na nuvem vamos precisar criar uma conta na AWS. Com a conta criada, é gerado para você um ID da sua conta que se encontra no botão "Pessoal", no canto superior direito da tela (ele será utilizado a seguir).

Feito isso, temos que cadastrar nossas credenciais da AWS no nosso computador, para isso temos que baixar o AWS CLI aqui.

Será necessário pegar sua key e secret_key do usuário, vá até security Credentials.

Selecione a aba Chaves de acesso e crie uma nova secret_key

Sugiro seguir o passo a passo que a AWS disponibiliza no link. OBS: Na região digite seu servidor de preferência, caso esteja na dúvida você pode visualizar os disponíveis aqui, estou utilizando o "sa-east-1".

Configuração do projeto

Primeiro passo é criar uma aplicação HerbsJS (para isso sugiro que veja o tutorial em outro artigo onde está tudo bem explicadinho aqui!).

Com o projeto criado, teremos uma estrutura parecida com essa (estou utilizando o VsCode como IDE).

Vamos apagar alguns templates e deixar o projeto um pouco mais clean!

Importante manter as pastas entities e usecases pois são nelas que criamos a regra de negócio da nossa aplicação.

Para transformar esse projeto em uma aplicação serverless, vamos utilizar o framework cujo nome é também Serverless.

Para instalar o framework serverless globalmente, rode o comando npm i serverless -g. Em seguida instale-o no projeto com o comando npm i serverless.

Vamos instalar também o sdk da AWS, um plugin que nos permite utilizar arquivos de environment, um para testar a aplicação localmente e um para testar a fila localmente respectivamente: npm i aws-sdk, npm i serverless-dotenv-plugin serverless-offline@8 serverless-offline-sqs@6 --save-dev.

Vamos primeiro configurar nosso projeto, criando um arquivo chamado .env na sua raiz. Nomeie a fila como desejar. OBS: É necessário colocar a linha NODE_ENV=test para o plugin serverless-offline entender que o ambiente é local.

NODE_ENV=test
SQS_ACCOUNT_URL_ID=http://localhost:9324/000000000000
SQS_QUEUE_NAME=fila-api-lambda

No arquivo index.js da raiz do projeto, vamos adicionar a referência aos arquivos das funções lambda.

module.exports = require('./src/infra/serverless')
index.js

Na pasta src do projeto, vamos criar uma pasta chamada config com um index.js e um arquivo chamado sqs.js

module.exports = {
  sqs: require('./sqs')
}
src > config > index.js
module.exports.filaLambdaApi = Object.freeze({ URL: process.env.SQS_URL })
src > config > sqs.js

Para criarmos um serverless vamos criar um arquivo chamado serverless.yml na raiz do projeto que vai conter todas as nossas configurações da fila e as lambdas que serão criadas no momento do deploy:

service: herbs-api-lambda

plugins:
  - serverless-offline
  - serverless-offline-sqs
  - serverless-dotenv-plugin

useDotenv: true
configValidationMode: error

provider:
  name: aws
  runtime: nodejs16.x
  stage: ${opt:stage, 'dev'}
  region: sa-east-1
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - sqs:SendMessage
          Resource: arn:aws:sqs:${self:provider.region}:*:${env:SQS_QUEUE_NAME}
  environment:
    NODE_ENV: ${env:NODE_ENV}
    SQS_URL: ${env:SQS_ACCOUNT_URL_ID}/${env:SQS_QUEUE_NAME}
    SQS_QUEUE_NAME: ${env:SQS_QUEUE_NAME}

functions:
  apiGateway:
    name: herbs-api
    handler: index.apiHandler
    events:
      - http:
          path: adicionarFila
          method: post
  sqsFila:
    name: herbs-consumer
    handler: index.consumerHandler
    events:
      - sqs:
          arn:
            Fn::GetAtt:
              - fila
              - Arn
          batchSize: 1

resources:
  Resources:
    fila:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: fila-api-lambda

custom:
  serverless-offline-sqs:
    autoCreate: true
    apiVersion: '2012-11-05'
    endpoint: http://0.0.0.0:9324
    region: sa-east-1
    accessKeyId: root
    secretAccessKey: root
    skipCacheInvalidation: false
serverless.yml

Explicando um pouco melhor cada comando do arquivo:

  • plugins: Especificando os plugins adicionais do projeto (no nosso caso, o plugin que permite utilizar o arquivo .env e o que permite testar offline).
  • useDotenv: Liberar o uso de arquivos de environment.
  • configValidationMode: Nível de aviso de erros na compilação.
  • provider: Aqui, ficam as configurações do serviço de cloud que estamos utilizando, versão do node, qual ambiente, região e configurações de environment e de permissões. No nosso caso, estamos liberando a permissão de enviar mensagens para a fila.
  • functions: Declaração das lambdas e seus eventos de gatilho.
  • resources: recursos que serão utilizados pelas lambdas e aqui definimos propriedades da fila.
  • custom: Configurações adicionais, geralmente de plugins.

Para mais informações, acesse a documentação do framework serverless aqui!

Criação da camada de infra

Agora, vamos olhar para nossa camada de infra para conectar nossa API lambda (que já criaremos). Vamos precisar ter um método que vai fazer o envio para a fila. Para isso, criaremos um client que faz o acesso ao SQS da AWS.

const { Err, Ok } = require('@herbsjs/herbs')

const dependencies = {
  SQS: require("aws-sdk").SQS
};

class AwsSQSClient {
  constructor(injection) {
    this._di = { ...dependencies, ...injection };
    this._sqs = new this._di.SQS();
  }

  async addMessage(body, URL) {
    const params = {
      MessageBody: JSON.stringify(body),
      QueueUrl: URL,
    };
    const request = await this._sqs.sendMessage(params);
    const result = await request.promise();
    if (result.MessageId) return Ok(result.MessageId);
    console.log(result);
    return Err(result);
  }
}

module.exports = AwsSQSClient
src > infra > clients > AWS > SQS.js

Esta classe envia a mensagem para a fila e, para não ferir o segundo conceito do SOLID e deixar nossa aplicação aberta a mudanças, vamos expor esse método em uma service para os use cases conseguirem acessar.

const dependencies = {
  SQS: require('../clients/AWS/SQS')
}

class QueueService {
  constructor(injection) {
    const di = Object.assign({}, dependencies, injection)
    this._sqsClient = new di.SQS()
  }

  async adicionarMensagem(mensagem, queueUrl) {
    return await this._sqsClient.addMessage(mensagem, queueUrl)
  }
}

module.exports = QueueService
module.exports.QueueService = QueueService
src > infra > services > QueueService.js

Ainda na camada de infra, vamos criar nossas funções serverless lambda e para isso criaremos uma pasta serverless.

A API será responsável por expor um endpoint para enviarmos dados para a fila. Como iremos criar um endpoint POST, no parâmetro event vem o body da requisição, caso fosse um GET, viria os QueryParams.

const { AdicionarFilaUseCase } = require('../../domain/usecases/AdicionarFilaUseCase')
const config = require('../../config');

const dependencies = {
  fila: config.sqs.filaLambdaApi
}

module.exports = async (event) => {

  try {
    const params = {
      body: event.body,
      filaUrl: dependencies.fila.URL
    }
    const useCase = AdicionarFilaUseCase();
    const ucResultado = await useCase.run(params)
    console.log(ucResultado)
    if (ucResultado?.isErr)
      throw new Error(ucResultado)
    return {
      statusCode: 200,
      body: JSON.stringify({
        messageId: ucResultado.value
      })
    }
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({
        message: "Erro - Ocorreu um erro ao enviar mensagem à fila",
        error: error.message
      })
    }
  }
};
src > infra > serverless > apiHandler.js

O consumidor será responsável por receber os dados da fila, que vem na variável event exposta pela AWS.

module.exports = async (event, context) => {
  for (const { body } of event.Records) {
    console.log(JSON.parse(body))
  }
}
src > infra > serverless > consumerHandler.js

E um arquivo index.js para expor essas funções.

module.exports = {
  apiHandler: require('./apiHandler'),
  consumerHandler: require('./consumerHandler')
}
src > infra > serverless > index.js

Com isso, nossa camada de infra estará assim:

Domínio

Note que na classe apiHandler chamamos um use case e nele está nossa regra de negócio (que no momento é só jogar o body da requisição na fila).

const { usecase, step, Ok } = require("@herbsjs/herbs");

const dependency = {
  QueueService: new (require('../../infra/services/QueueService')),
}

const AdicionarFilaUseCase = (injection) =>
  usecase("Adicionar mensagem à fila", {
    request: {
      body: String,
      filaUrl: String,
    },

    setup: (ctx) => (ctx.di = Object.assign({}, dependency, injection)),

    "Envia mensagem para fila": step(async (ctx) => {
      const { body, filaUrl } = ctx.req
      const resultadoEnvioMensagem = await ctx.di.QueueService.adicionarMensagem(body, filaUrl)
      ctx.ret = resultadoEnvioMensagem.value
      return Ok();
    }),
  });

module.exports = AdicionarFilaUseCase
module.exports.AdicionarFilaUseCase = AdicionarFilaUseCase
src > domain > usecases > AdicionarFilaUseCase.js

Este use case chama nossa service que adiciona a mensagem à fila do SQS.

No fim, devemos ter esses arquivos no projeto:

Rodar o Projeto Localmente

Para rodar o projeto localmente, é necessário configurar a fila local com o ElasticMQ através do docker, caso não tenha instalado o docker, instale aqui. Após instalado o docker, abra seu cmd ou PowerShell e rode o comando

docker run -p 9324:9324 -p 9325:9325 softwaremill/elasticmq

E pronto! Temos rodando nosso simulador de filas! Entre em http://localhost:9325/ e conseguirá visualizar graficamente o simulador.

Com a fila criada, podemos rodar nossa API localmente com o comando serverless offline. Assim, conseguimos observar nosso endpoint POST e nosso consumer rodando.

Enviaremos uma requisição através do Postman ou seu Client API de preferência. Vamos mandar o seguinte body na requisição:

{
    "Teste": "Serverless com HerbsJS"
}

Ao enviar a requisição recebemos a confirmação 200 com o id da mensagem na fila.

Voltando ao VsCode, podemos observar que o endpoint foi chamado e o consumidor logou o body da requisição:

Deploy das Aplicações

Para fazer o deploy vamos criar outro arquivo de environment com o sufixo de produção ficando .env.production, pegando o ID da sua conta AWS e substituindo a string AWS_ACCOUNT_ID:

NODE_ENV=production
SQS_ACCOUNT_URL_ID=https://sqs.sa-east-1.amazonaws.com/AWS_ACCOUNT_ID
SQS_QUEUE_NAME=fila-api-lambda
.env.production

Para auxiliar no deploy com o environment de produção, vamos adicionar a biblioteca cross-env com o comando npm i cross-env. Em seguida, é necessário adicionar o comando nos scripts do arquivo package.json

"scripts": {
    ...
    "deploy": "cross-env NODE_ENV=production serverless deploy --stage production"
}

Neste comando, estamos definindo o arquivo de environment e o stage da AWS como produção. Agora, iremos rodar o comando npm run deploy.

Como estamos rodando tudo localmente, provavelmente você terá este erro da imagem:

Isso acontece porque quando o node está fazendo o deploy para a AWS ele carrega todas as dependências do projeto, porém há um limite de 1024 dependências, assim gerando este erro EMFILE: too many open files. Caso o deploy seja feito por um pipeline CD em um servidor, isso não acontecerá, mas para nosso caso há um jeito de burlar essa regra do node.

Existe uma biblioteca chamada graceful-fs que faz um buffer de carregamento de arquivos, e vamos substituir a biblioteca fs que é padrão do deploy do serverless. Assim, conseguindo fazer um buffer do carregamento de arquivos permitindo que todas as dependências sejam carregadas.

  • Instalar o graceful-fs: npm i graceful-fs
  • Vá até o arquivo: node_modules/serverless/lib/plugins/package/lib/zip-service.js
  • Substitua essa linha:
const fs = BbPromise.promisifyAll(require('fs'));

Por:

var realFs = require('fs')
var gracefulFs = require('graceful-fs')
gracefulFs.gracefulify(realFs)
const fs = BbPromise.promisifyAll(realFs)

Agora, rodamos novamente o comando npm run deploy.

Após publicado, receberemos uma url que é onde nossa api está hospedada na AWS. Se pegarmos essa url e substituirmos no Postman a url http://localhost:3000, estaremos batendo direto na AWS nas nossas aplicações.

Para visualizar o log do consumer, podemos chamar o log dele através do comando serverless logs -f sqsFila -t --stage production. Assim, ele ficará observando os dados da fila.

E ao enviar pelo postman, ele loga no console.

Você consegue visualizar as lambdas e os gatilhos entrando na AWS, procurando por lambda na busca.

E ao entrar em cada uma, é possível ver seus gatilhos: na API é possível ver o gatilho da API Gateway e no consumer é possível ver o gatilho da fila SQS.

Se por algum acaso tenha perdido a url de produção, é só ir até a lambda de API e clicar no gatilho de API Gateway que será mostrado para você.

Conclusão

Pronto! Você aprendeu a criar uma aplicação com mensageria utilizando Lambdas Serverless e SQS para a fila, tudo isso utilizando o HerbsJS que aumenta sua produtividade no dia a dia. Além disso, fez deploy em produção da aplicação e testou localmente. Agora o céu é o limite, escale seus microsserviços e seja feliz!

Caso queira o código fonte deste artigo, basta acessar aqui.