Transforme sua Rotina de Desenvolvimento: AWS Local com LocalStack e Node.js

Transforme sua Rotina de Desenvolvimento: AWS Local com LocalStack e Node.js

Introdução

Se você, assim como eu, lida com o desenvolvimento de soluções baseadas na cloud da Amazon (AWS), sabe que temos desafios particulares quanto a testar nossas aplicações e garantir que o funcionamento que temos em ambiente local é próximo ao que podemos esperar em produção. Por isso, hoje venho compartilhar com vocês uma ferramenta muito interessante que tem me auxiliado nesse processo: LocalStack.

Neste guia prático, mergulharemos no universo dessa ferramenta, explorando sua integração fluida com o Docker para proporcionar uma experiência de desenvolvimento eficaz e que pode agilizar bastante nosso trabalho. Iremos explorar alguns conceitos básicos e exemplificar tudo com uma aplicação Node.js que faz uso dos serviços SQS e SNS. Vamos juntos desbravar passo a passo como otimizar seu fluxo de desenvolvimento local de maneira descomplicada.

Sobre a LocalStack

LocalStack é um projeto de código aberto projetado para ser um stack AWS local totalmente funcional, voltado para desenvolvimento e testes. Ele permite que desenvolvedores testem suas aplicações localmente, eliminando a necessidade de incorrer em custos associados à execução de recursos na AWS durante o desenvolvimento. Seja para trabalhar em aplicações complexas com CDK, configurações com Terraform ou explorar serviços da AWS, o LocalStack acelera e simplifica o fluxo de trabalho de teste e desenvolvimento.

A ferramenta atualmente oferece suporte a uma variedade de serviços da AWS, como AWS Lambda, S3, DynamoDB, Kinesis, SQS, SNS, etc. O LocalStack opera como um emulador de serviços em nuvem em um único contêiner Docker na sua máquina local. Isso possibilita a execução, teste e depuração de aplicações AWS sem a necessidade de uma conexão com um provedor de nuvem remoto.

Configuração do ambiente

No nosso exemplo, vamos utilizar o Docker para provisionarmos o LocalStack e o Node JS para construírmos uma aplicação simples que utiliza dois serviços da AWS: SQS e SNS. Além disso, vamos construir um script bash que será executado durante a inicialização do container e criará - utilizando comandos da AWS CLI - os serviços e configurações que desejamos.

Começando pelo mais importante, vamos construir um arquivo docker-compose que expõe o serviço do LocalStack, com isso, podemos garantir que teremos um ambiente de desenvolvimento similar para todos os contribuintes no projeto.

version: '3.7'

services:
  localstack:
    container_name: "localstack"
    image: localstack/localstack
    ports:
      - "127.0.0.1:4566:4566"
    environment:
      - SERVICES=sqs
      - EDGE_PORT=4566
      - DEBUG=1
      - DOCKER_HOST=unix:///var/run/docker.sock
    volumes:
      - ./localstack_setup:/etc/localstack/init/ready.d
      - /tmp/localstack:/tmp/localstack
      - /var/run/docker.sock:/var/run/docker.sock

O código acima faz uso de uma imagem Docker da LocalStack e cria - dentre suas configurações padrão - um link para o script bash que criará os recursos na LocalStack e o script de inicialização do container: ./localstack_setup:/etc/localstack/init/ready.d

#!/usr/bin/env bash

set -euo pipefail

echo "configuring sns/sqs"
echo "==================="

LOCALSTACK_HOST=localhost
AWS_REGION=us-east-1
LOCALSTACK_DUMMY_ID=000000000000

create_queue() {
    local QUEUE_NAME_TO_CREATE=$1
    awslocal --endpoint-url=http://${LOCALSTACK_HOST}:4566 sqs create-queue --queue-name "${QUEUE_NAME_TO_CREATE}" --region ${AWS_REGION} --output text
}

create_topic() {
    local TOPIC_NAME_TO_CREATE=$1
    awslocal --endpoint-url=http://${LOCALSTACK_HOST}:4566 sns create-topic --name "${TOPIC_NAME_TO_CREATE}" --output text
}

guess_queue_arn_from_name() {
    local QUEUE_NAME=$1
    echo "arn:aws:sns:${AWS_REGION}:${LOCALSTACK_DUMMY_ID}:$QUEUE_NAME"
}

link_queue_and_topic() {
    local TOPIC_ARN_TO_LINK=$1
    local QUEUE_ARN_TO_LINK=$2
    awslocal --endpoint-url=http://${LOCALSTACK_HOST}:4566 sns subscribe --topic-arn "${TOPIC_ARN_TO_LINK}" --protocol sqs --notification-endpoint "${QUEUE_ARN_TO_LINK}" --output table
}

# Nomes das filas e tópicos
ORDERS_QUEUE="orders"
CREATE_NEW_ORDER_TOPIC="create-new-order"


echo "Criando tópico $CREATE_NEW_ORDER_TOPIC"
NEW_ORDER_TOPIC_ARN=$(create_topic ${CREATE_NEW_ORDER_TOPIC})
echo "Tópico criado: $NEW_ORDER_TOPIC_ARN"

echo "Criando fila $ORDERS_QUEUE"
ORDERS_QUEUE_URL=$(create_queue ${ORDERS_QUEUE})
echo "Fila criada: $ORDERS_QUEUE_URL"
ORDERS_QUEUE_ARN=$(guess_queue_arn_from_name "$ORDERS_QUEUE")

echo "Associando tópico $NEW_ORDER_TOPIC_ARN à fila $ORDERS_QUEUE_ARN"
LINKING_RESULT=$(link_queue_and_topic "$NEW_ORDER_TOPIC_ARN" "$ORDERS_QUEUE_ARN")
echo "Associação realizada:"
echo "$LINKING_RESULT"

bootstrap.sh

Nesse script, configuramos 4 funções:

  • create_queue para criação de queues SQS.
  • create_topic para criação de tópicos SNS.
  • guess_queue_arn_from_name para buscar o Amazon Resource Name (ARN) de uma fila.
  • link_queue_and_topic para inscrever nossa queue SQS a um tópico SNS.

Adicionando o Node JS

Para obter acesso ao exemplo completo, basta seguir para este repositório. Aqui, irei demonstrar apenas os recursos básicos de integração do Node JS com os serviços da AWS, sem tratar as especificidades de cada recurso.

Na configuração da fila SQS, basicamente criaremos um arquivo onde iremos criar uma instância do SQS Client a partir das configurações da nossa infra local com LocalStack:

const client = new SQSClient({
  region: "us-east-1",
  endpoint: "http://localhost:4566",
  credentials: {
    accessKeyId: "test",
    secretAccessKey: "test",
  },
});

export default class SQSProdConsMessageBroker
  implements IProdConsMessageBroker
{
  async read(queue: string) {
    const receiveMessageCommand = new ReceiveMessageCommand({
      MaxNumberOfMessages: 10,
      QueueUrl: queue,
      WaitTimeSeconds: 5,
      MessageAttributeNames: ["All"],
    });

    const response = await client.send(receiveMessageCommand);

    if (response.Messages && response.Messages.length > 0) {
      return {
        body: response.Messages[0]?.Body,
        receiptHandle: response.Messages[0].ReceiptHandle,
      };
    }

    console.log("Waiting for messages...");
    return;
  }
  async deleteMessage(queue: string, receiptHandle: string): Promise<void> {
    const command = new DeleteMessageCommand({
      QueueUrl: queue,
      ReceiptHandle: receiptHandle,
    });

    await client.send(command);
  }
}

sqs.ts

Para o tópico SNS, temos uma configuração semelhante:

const client = new SNSClient({
  region: "us-east-1",
  endpoint: "http://localhost:4566",
});

export default class SNSPubSubMessageBroker implements IPubSubMessageBroker {
  async publish(body: any, TopicArn: string): Promise<string | undefined> {
    const publishParams = {
      TopicArn,
      Message: JSON.stringify(body),
    };
    const command = new PublishCommand(publishParams);
    try {
      const publishedMessage = await client.send(command);

      return publishedMessage.MessageId;
    } catch (e) {
      console.log("an error occurred: ", e);
    }
  }
}

sns.ts

Como podemos ver, o LocalStack permite que configuremos nossos recursos com as funções nativas da AWS, alterando apenas as variáveis de ambiente que determinam onde está o provider (LocalStack / Cloud AWS).

Além disso, ao termos um ambiente dockerizado podemos permitir que todos os devs possam startar o projeto sem grandes problemas, e, principalmente, sem a necessidade de sempre configurar os recursos que serão provisionados pela LocalStack.

Executando o código

Com nosso projeto devidamente configurado no docker-compose + bash, podemos executar o exemplo (disponível neste repositório).

# devemos ceder permissão para que o arquivo bootstrap.sh seja executado
chmod +x ./localstack_setup/bootstrap.sh

# executamos nossa imagem do localstack
docker-compose up -d

# damos start no projeto
npm run start:dev

Com o projeto em execução, nosso console irá apresentar a seguinte mensagem: Waiting for messages.... Para que a fila seja notificada, podemos enviar mensagens para o endpoint http://localhost:8080/create:

POST http://localhost:8080/create HTTP/1.1
content-type: application/json

{
    "table": "14D",
    "products": [
        {"name": "coca-cola", "quantity": 2}
    ]
}

Após a execução de requisições, o console deve apresentar o resultado que foi recebido por nossa fila SQS.

Conclusão

Em resumo, a LocalStack oferece uma solução prática e conveniente para o desenvolvimento de aplicações que fazem uso de recursos provisionados pela AWS. É uma ferramenta particularmente útil quando buscamos agilidade, principalmente para testarmos alguns serviços pontuais, como o sistema de filas e notificações.

No mais, acho importante ressaltar que a confiabilidade do nosso projeto vai muito além dessas facilidades no ambiente local. As validações mais importantes ainda devem ser feitas na nuvem, e para isso, o desenvolvedor precisa ter domínio das estruturas principais que compõem seu ambiente de deployment.