Enviando mensagens SMS agendadas com AWS EventBridge, Pinpoint e Lambda usando Serverless Framework

Você pegou um trampo freela onde seu cliente pediu que o sistema enviasse para ele via SMS um mini relatório todos os dias as 18 horas.

Esse mini relatório pode ser número de visualizações numa landing page, o total de vendas em cartão de crédito, cartão de débito, dinheiro e vale refeição, ou a cotação do dólar.

Nesse texto mostro como você pode implementar isso super rápido usando alguns serviços da AWS e o Serverless Framework, entregando para ele a funcionalidade pronta em algumas horas.

Ferramentas utilizadas e para que servem

AWS EventBridge

Um disparador de eventos cheio de funcionalidades, que pode ser integrado com sistemas SAAS,
pode jogar o evento para vários outros serviços da AWS e é super escalável, como não poderia deixar de ser.

Nesse texto vamos usá-lo só como um cron para disparar um lambda mesmo. 🙂
Poderíamos até usar o CloudWatch, mas eu achei o nome EventBridge mais chique e não somos meteorologistas para ficar assistindo a nuvem. 😉

AWS Pinpoint

Esse também é um serviço mega ultra escalável que permite criar campanhas de engajamento através de diversos canais de comunicação (SMS, email, push notification).

Ele também tem umas paradas maneiras que analisam como os usuários interagem com o aplicativo e tem até inteligência artificial.

Aqui vamos usá-lo só pra enviar SMS de graça mesmo, pois dá pra enviar 5000 por mês sem gastar cents. 🙂

AWS Lambda

Esse aqui é aquele famosinho que você copia / cola o código no editor ou sobe um arquivo zip e ele executa esse código conforme algum evento.
Seja API Gateway (via HTTP ou Websocket), SQS ou… EventBridge! 🙂

Nossa, tava quase esquecendo de falar que você não precisa se preocupar com provisionamento de servidor.

Serverless framework

Esse aqui é maneiro! 😀
Trata-se de um framework para criar aplicações serverless — Jura? — Em vários provedores de nuvem diferentes usando arquivos YAML e alguns comandinhos.

Arquivos YAML são fáceis de escrever (exceto o CloudFormation) e fáceis de ler (exceto o CloudFormation) e os comandos para criar, deployar,
deletar, ver métricas, logs e tudo mais da aplicação são bem tranquilos.

Entregando o projeto

Antes de mais nada, Você precisa ter o NodeJS (a função será escrita em Javascript e o Serverless framework também é escrito nessa linguagem) e
a ferramenta de linha de comando da AWS configuradinha.

Caso você não tenha familiaridade com esses pré-requisitos, veja essa seção da documentação do Serverless framework (Qualquer dúvida só chamar).

Iniciando o projeto

# Criar e mudar para a pasta do projeto
$ mkdir reporter
$ cd reporter
# Instalar o serverless globalmente para ter acesso ao comando de qualquer lugar
$ npm install -g serverless
# Iniciar um novo projeto NodeJS
$ npm init -y
# Instalar as dependências do projeto
# Não é estritamente necessário instalar o aws-sdk, pois ele está disponível no ambiente da AWS. Mas é bom deixar as dependências explícitas
$ npm install --save aws-sdk
# Instalar as dependências de desenvolvimento
# instalamos o serverless localmente também para deixar a dependência explícita e para que o comando sempre rode a versão que está na pasta do projeto
# os módulos dotenv e serverless-dotenv-plugin são úteis para setarmos as variáveis de ambiente num arquivo .env.
$ npm install --save-dev serverless dotenv  serverless-dotenv-plugin

Variáveis de ambiente nos arquivos .env / .env.dist

Nosso lambda será responsável por enviar apenas um SMS para um número específico quando for invocado.
Para não deixar esse número direto no código e nem commitar no repositório criamos o arquivo .env.dist com valores em branco.

Também vamos ter que referenciar a região dos serviços que estamos utilizando em alguns pontos do projeto.
Para não repetir esses valores, deixamos em variável de ambiente!

$ nano .env.dist
REGION=us-east-1
SEND_TO_PHONE_NUMBER=

ctrl+x para salvar e voltar ao terminal

Quando formos deployar a aplicação, o framework irá procurar pelo arquivo .env.
Vamos agora criar o arquivo .env que será uma cópia do .env.dist, com os valores de verdade.
O .env não deve ser commitado no repositório do código.

$ cp .env.dist .env
$ nano .env
REGION=us-east-1
SEND_TO_PHONE_NUMBER=+5511998765432

ctrl+x para salvar e voltar ao terminal

Repare que o número do telefone deve começar com o sinal de mais (+), seguido pelo código do país (55 – Brasil) e o número com DDD, sem zero.

.gitignore

Falei aí de repositório, e etc. Vamos só criar o arquivo .gitignore para que o git não rastreie certos arquivos e pastas:

$ nano .gitignore
node_modules
jspm_packages
.serverless
.env

ctrl+x para salvar e voltar ao terminal

Pronto. Agora se você quiser versionar esse projeto no git não corre risco de commitar arquivos desnecessários.

Código do nosso lambda

Vamos ver o código do nosso lambda, que é bem tranquilo. Vou comentar algumas partes para ficar legal:

$ nano index.js
const AWS = require('aws-sdk');

exports.handler = async (event, context) => {
  // Variáveis iniciais: instância do Pinpoint, número de telefone, ID da aplicação
  const pinpoint = new AWS.Pinpoint({
    region: process.env.REGION,
  });
  const pinpointApplicationId = process.env['PINPOINT_APPLICATION_ID'];
  const phoneNumber = process.env['SEND_TO_PHONE_NUMBER'];
  // Vamos mockar o que seria uma resposta de API
  const dolarInfo = {
    brl: 4.65,
  };
  // Texto do SMS
  const text = `Hoje o dolar fechou a R$ ${dolarInfo.brl}`;
  // Parâmetros do SMS
  const messageParams = {
    ApplicationId: pinpointApplicationId,
    MessageRequest: {
      Addresses: {
        [phoneNumber]: {
          ChannelType: 'SMS',
        },
      },
      MessageConfiguration: {
        SMSMessage: {
          // TRANSACTIONAL - mensagem importante
          // PROMOTIONAL - mensagem de marketing
          MessageType: 'PROMOTIONAL',
          Body: text,
        },
      },
    },
  };

  try {
    // Envia o trem!
    await pinpoint.sendMessages(messageParams).promise();
    console.log(`SMS enviado com sucesso: ${text}`);
  } catch(error) {
    console.error(`Erro ao enviar SMS: ${error.message}`);
  }
};

ctrl+x para salvar e voltar ao terminal

O código acima é o responsável por pegar a informação que o cliente deseja de alguma API (aqui mockamos) e enviar o SMS.

Note que usamos as duas variáveis de ambiente definidas.
No momento do deploy, o serverless framework irá pegar todos os valores que estiverem no arquivo .env e gravá-los como variáveis de ambiente para o executor do lambda.

Definindo as configurações e deployando

Agora chegamos na parte legal. Vamos usar o sls para juntar todas as pecinhas que temos até agora e que ainda não fazem nada na funcionalidade que seu cliente quer.

especificando quais recursos sua aplicação irá usar — arquivo serverless-aws-resources.yml

Antes de continuar, é interessante dizer que podemos escrever tudo num arquivo só, o serverless.yml.
Mas eu gosto de separar um pouquinho as coisas para não ficar um arquivo gigante e cheio de informação.

No arquivo serverless-aws-resources.yml iremos definir os recursos que nosso lambda vai precisar para executar com sucesso.
Esses recursos podem ser buckets do AWS S3, tabelas do DynamoDB, etc.

Aqui precisaremos de dois recursos bem simples de serem criados:
Uma aplicação Pinpoint (que irá gerenciar nosso envio de mensagens), e o canal de envio de SMS do Pinpoint (que estará vinculado a essa aplicação).

Para deixar mais claro, uma aplicação Pinpoint pode enviar mensagens por diversos canais de comunicação.
Então vamos criar a aplicação e adicionar o canal que desejamos usar (SMS) a ela.
Em aplicações mais complexas pode ser preciso adicionar mais de um canal, como email e push notification, por exemplo.

Sem mais enrolação, vamos ao arquivo serverless-aws-resourses.yml:

Resources:
  pinpointApp:
    Type: 'AWS::Pinpoint::App'
    Properties:
      Name: reporter

  pinpointSmsChannel:
    Type: 'AWS::Pinpoint::SMSChannel'
    Properties:
      # Aqui especificamos a qual aplicação esse recurso pertence.
      # "Ref" é uma função do CloudFormation (linguagem de criação de recursos AWS) que é útil para pegar o ID de outros recursos.
      ApplicationId:
        Ref: pinpointApp

Esse arquivo ainda me parece tranquilo.
Definimos os recursos citados, e o que temos aí de especial é o uso da função Ref do CloudFormation.

O arquivo que faz a mágica — serverless.yml

Esse arquivo é o que será lido pelo sls. Vamos criá-lo de forma incremental.

Definindo metadados
service: reporter
frameworkVersion: '2'

plugins:
  - serverless-dotenv-plugin

resources:
  - '${file(serverless-aws-resources.yml)}'

Aqui definimos alguns dados básicos do nosso serviço, como o nome, qual versão do sls estamos usando e quais plugins.
Também fazemos uma referência a nosso arquivo serverless-aws-resources.yml na seção “resources”, que é utilizada para especificar quais recursos o serviço irá precisar.
O que eu não te contei ainda é que aquele arquivo está no formato do CloudFormation, a linguagem de definição de recurso da AWS; então tudo que está ali é processado diretamente pela AWS.

Opções do provedor (AWS)

Aqui vamos definir qual provedor usamos, qual runtime nossa função lambda irá executar e especificar as permissões que nosso serviço precisa. Vamos começar do começo:

provider:
  name: aws
  runtime: nodejs12.x
  region: '${env:REGION}'

Essa é a parte mais tranquila. Definimos o provedor, runtime (NodeJS), e a região do serviço, definido no arquivo .env; lembra dele?

Permissões necessárias para o serviço

A AWS tem a boa política de que uma função deve ter permissão para fazer só o que for necessário.
Se você não permitir que ela logue no CloudWatch nem isso ela vai fazer, e olha que log é meio trivial.

A questão do log o sls já resolve pra gente, definindo a permissão de log para as funções criadas por ele.
Aqui iremos permitir que nossa função utilize o serviço de envio de mensagens da aplicação Pinpoint que criamos anteriormente.

Não se perca pelos colchetes e chaves 🙂

  iamRoleStatements:
    -
      Effect: Allow
      Action:
        - 'mobiletargeting:SendMessages'
      Resource:
        'Fn::Join': [':', ['arn:aws:mobiletargeting', '${env:REGION}', { Ref: 'AWS::AccountId' }, { 'Fn::Join': [/, [apps, { Ref: pinpointApp }, messages]] }]]

A política acima está permitindo (Allow):

  • que nosso serviço chame a função mobiletargeting:SendMessages do serviço mobiletargeting
  • da aplicação que criamos anteriormente na região que definimos na variável de ambiente
  • do usuário atual

A função ‘FN::Join’ serve para juntar um array por um separador, é algo como o join das outras linguagens de programação.
'Fn::Join: [/, [Josiel, Santos]]' retorna “Josiel/Santos”.

Em YAML isso pode ser escrito mais verborrágico, porém eu sinceramente me perdi com tanta linha.
Abaixo está o formato alternativo:

  iamRoleStatements:
    - Effect: 'Allow'
      Action:
        - mobiletargeting:SendMessages
      Resource:
        Fn::Join:
          - ':'
          -
            - 'arn:aws:mobiletargeting'
            - ${env:REGION}
            - Ref: 'AWS::AccountId'
            - Fn::Join:
              -  '/'
              -
                - 'apps'
                - Ref: 'pinpointApp'
                - 'messages'

Em tempo, o retorno da função acima será algo como: arn:aws:mobiletargeting:us-east-1:123456:apps/654321/messages.

Criando nossa função e especificando a qual evento ela vai escutar
functions:
  # Função 'sendDolarInfoViaSms' que será executada pela função 'handler' em index.js
  sendDolarInfoViaSms:
    handler: index.handler
    events:
      -
        # Executa de segunda a sexta, as 18:00
        eventBridge: { schedule: 'cron(0 18 ? * MON-FRI *)' }
    # sls, por favor, deixe essas variáveis que eu definir aqui disponíveis para minha função Javascript; Obrigado.
    environment:
      REGION: '${env:REGION}'
      PINPOINT_APPLICATION_ID:
        Ref: pinpointApp
      SEND_TO_PHONE_NUMBER: '${env:SEND_TO_PHONE_NUMBER}'

Aqui criamos a função sendDolarInfoViaSms. O responsável por executá-la é a função handler do arquivo index.js; lembra dele?
Ela deverá ser executada de segunda a sexta as 18 horas, e o sls irá passar
para ela tudo aquilo que está definido na seção environments, que novamente usamos a função Ref e pegamos a variável de ambiente usando uma variável especial do sls, a ${env:var_name}.

Arquivo serverless.yml completo
service: reporter
frameworkVersion: '2'

plugins:
  - serverless-dotenv-plugin

resources:
  - '${file(serverless-aws-resources.yml)}'

provider:
  name: aws
  runtime: nodejs12.x
  region: '${env:REGION}'
  iamRoleStatements:
    -
      Effect: Allow
      Action:
        - 'mobiletargeting:SendMessages'
      Resource:
        'Fn::Join': [':', ['arn:aws:mobiletargeting', '${env:REGION}', { Ref: 'AWS::AccountId' }, { 'Fn::Join': [/, [apps, { Ref: pinpointApp }, messages]] }]]

functions:
  sendDolarInfoViaSms:
    handler: index.handler
    events:
      -
        eventBridge: { schedule: 'cron(0 18 ? * MON-FRI *)' }
    environment:
      REGION: '${env:REGION}'
      PINPOINT_APPLICATION_ID:
        Ref: pinpointApp
      SEND_TO_PHONE_NUMBER: '${env:SEND_TO_PHONE_NUMBER}'

Faz o deploy aí pra mim na moral, tio?

Agora, vamos executar o comando sls deploy.

Esse comando irá criar o arquivo zip da nossa função, juntamente com as dependências de produção — Ninguém quer um arquivo com 87382938 MB quando poderia ter apenas 9 –,
também vai gerar os templates CloudFormation para criar os recursos na AWS e, se tudo correr bem, o serviço será criado.

Se você não quiser esperar até as 18:00, mude o número de telefone para o seu no arquivo .env, e Rode o comando sls invoke local -f sendDolarInfoViaSms .
Para verificar os erros, sls logs -f sendDolarInfoViaSms pode te ajudar.

Fim

Estava esperando mais passos?
Sinto desapontá-lo.

O sls também é uma plataforma maneira que pode ajudar deployando seu serviço automaticamente assim que ocorrer um push no seu repositório, tem um dashboard de métricas bonitinho e etc.
Como aqui não tivemos essas necessidades, não mexemos com isso, mas claramente vale apena explorar tudo o que essas ferramentas tem a oferecer.

Eu espero que você tenha gostado desse texto e que, de alguma maneira tenha te ajudado.
Qualquer dúvida, sugestão, reclamação, xingamento ou elogio deixe nos comentários ou chama nas redes.

Valeu!

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *