Desenvolvimento na AWS (S3 e SQS) sem custos com Localstack

Gustavo Oliveira
6 min readApr 12, 2020

--

Há alguns dias eu desenvolvi um projeto que envolvia manipular arquivos num bucket do serviço S3 da AWS. A minha primeira ideia foi utilizar o armazenamento local durante o desenvolvimento. No entanto, eu também pretendia disparar notificações para uma fila no SQS ao criar novos objetos no bucket. Este cenário não é facilmente simulado, logo, eu teria que utilizar os serviços da AWS durante o desenvolvimento, o que envolveria custos, já que meu período de testes havia expirado. Após pesquisar sobre alternativas sem custo, me deparei com o Localstack.

O Localstack é um projeto desenvolvido inicialmente pela Atlassian e, atualmente independente, que permite utilizar funcionalidades e APIs da AWS em um ambiente local. Desse modo, é possível diminuir o custo de projetos, utilizando o Localstack em ambiente de desenvolvimento e para a execução de testes em integração contínua.

Configurando o Localstack

Para configurar o Localstack é possível utilizar o docker-compose conforme o exemplo a seguir.

version: '3'
services:
localstack:
image: localstack/localstack
environment:
- SERVICES=s3,sqs
- DOCKER_HOST=unix:///var/run/docker.sock
ports:
- "4572:4572" # s3
- "4576:4576" # sqs

Os serviços a serem utilizados são definidos na variável de ambiente SERVICES separados por vírgula. A variável DEBUG=1 indica que o modo debug está ativo. Cada serviço da AWS disponível no Localstack é exposto em uma porta distinta do container especificada na documentação. Nesse exemplo, 4572 para o S3 e 4576 para o SQS.

Para iniciar o ambiente do arquivo docker-compose.yml utilize o comando

docker-compose up -d

e rode o container num shell interativo como bash com o comando

docker-compose exec localstack bash

A imagem do Localstack já possui o AWS CLI (Command Line Interface) instalado, mas é necessário configurá-lo antes de utilizar. Para tal, você pode executar

aws configure

e inserir as configurações manualmente, porém, caso o container seja recriado, será necessário executar novamente este comando. Uma forma mais eficiente é passar variáveis de ambiente reconhecidas pelo AWS CLI (https://docs.aws.amazon.com/pt_br/cli/latest/userguide/cli-configure-envvars.html) para o container através do docker-compose.yml. Segue um exemplo:

environment:
...
- AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
- AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
- AWS_DEFAULT_REGION=us-east-1
- AWS_DEFAULT_OUTPUT=json

As credenciais NÃO PRECISAM SER VÁLIDAS, mas devem ser definidas!

Configurando o S3 e SQS

Para criar um bucket no S3, execute

aws --endpoint-url=http://127.0.0.1:4572 s3api create-bucket --bucket mybucket

e adicione as regras de controle de acesso (ACL — Access Control List) ao bucket. No meu caso, eu utilizei public-read (https://docs.aws.amazon.com/pt_br/AmazonS3/latest/dev/acl-overview.html).

aws --endpoint-url=http://127.0.0.1:4572 s3api put-bucket-acl --bucket mybucket --acl public-read

A flag — endpoint-url é utilizada para rodar os comandos apontando para os endpoints das APIs do Localstack instaladas localmente em vez das APIs reais da AWS.

Para criar uma fila no SQL, execute

aws --endpoint-url=http://127.0.0.1:4576 sqs create-queue --queue-name myqueue

Agora podemos configurar as notificações de evento do S3 para enviar uma mensagem para a fila do SQS quando um objeto for criado no bucket. Podemos fazer isto com o comando

aws --endpoint-url=http://127.0.0.1:4572 s3api put-bucket-notification-configuration --bucket mybucket --notification-configuration file://notification.json

Este comando recebe no parâmetro — notification-configuration um arquivo JSON com as configurações de notificação, conforme o exemplo a seguir

{
"QueueConfigurations": [
{
"QueueArn": "arn:aws:sqs:us-east-1:000000000000:myqueue",
"Events": [
"s3:ObjectCreated:*"
]
}
]
}

O atributo QueueConfigurations recebe as configurações para disparo de notificações para filas do SQS, mas também é possível dispará-las para os serviços Lambda e SNS, conforme a documentação.

O atributo QueueArn representa o nome do recurso (ARN — Amazon Resource Name), que pode ser obtido com o comando

aws --endpoint-url=http://127.0.0.1:4576 sqs get-queue-attributes --queue-url http://localhost:4576/queue/myqueue --attribute-names All

O atributo Events recebe os tipos de eventos que devem gerar notificações. Para eventos de criação de objeto o tipo utilizado é s3:ObjectCreated:*.

Pronto. Para testar podemos criar um arquivo

touch sample.jpg

e fazer o upload para o bucket

aws --endpoint-url=http://127.0.0.1:4572 s3 cp sample.jpg s3://mybucket/

O comando abaixo listar as últimas mensagens recebidas no SQS

aws --endpoint-url=http://127.0.0.1:4576 sqs receive-message --queue-url http://localhost:4576/queue/myqueue --attribute-names All --message-attribute-names All

e devemos receber uma saída como a seguinte

{
"Messages": [
{
"MessageId": "a34dae12-7e29-4f04-a503-6c0e648bbc92",
"ReceiptHandle": "a34dae12-7e29-4f04-a503-6c0e648bbc92#b2afc966-08c8-4afe-af8b-4fa82deafad7",
"MD5OfBody": "8804d02d4a3a56bb41c612d3dd453208",
"Body": "{\"Records\": [{\"eventVersion\": \"2.0\", \"eventSource\": \"aws:s3\", \"awsRegion\": \"us-east-1\", \"eventTime\": \"2020-04-11T01:06:03.783Z\", \"eventName\": \"ObjectCreated:Put\", \"userIdentity\": {\"principalId\": \"AIDAJDPLRKLG7UEXAMPLE\"}, \"requestParameters\": {\"sourceIPAddress\": \"127.0.0.1\"}, \"responseElements\": {\"x-amz-request-id\": \"8b675a0d\", \"x-amz-id-2\": \"eftixk72aD6Ap51TnqcoF8eFidJG9Z/2\"}, \"s3\": {\"s3SchemaVersion\": \"1.0\", \"configurationId\": \"testConfigRule\", \"bucket\": {\"name\": \"mybucket\", \"ownerIdentity\": {\"principalId\": \"A3NL1KOZZKExample\"}, \"arn\": \"arn:aws:s3:::mybucket\"}, \"object\": {\"key\": \"sample.jpg\", \"size\": 0, \"eTag\": \"d41d8cd98f00b204e9800998ecf8427e\", \"versionId\": null, \"sequencer\": \"0055AED6DCD90281E5\"}}}]}",
"Attributes": {
"SentTimestamp": "1586567163843",
"ApproximateReceiveCount": "1",
"ApproximateFirstReceiveTimestamp": "1586567170230",
"SenderId": "127.0.0.1",
"MessageDeduplicationId": "",
"MessageGroupId": ""
}
}
]
}

Persistindo dados

Com a configuração que foi feita até agora, quando o container é reiniciado, os dados são perdidos. Para persisti-los podemos seguir o arquivo docker-compose.yml abaixo:

version: '3'
services:
localstack:
image: localstack/localstack
environment:
- SERVICES=s3,sqs
- DATA_DIR=/tmp/localstack/data
- DOCKER_HOST=unix:///var/run/docker.sock
# AWS
- AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
- AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
- AWS_DEFAULT_REGION=us-east-1
- AWS_DEFAULT_OUTPUT=json
ports:
- "4572:4572" # s3
- "4576:4576" # sqs
volumes:
- ./localstack/data:/tmp/localstack/data

A variável de ambiente DATA_DIR especifica onde os arquivos de persistência do Localstack devem ser armazenados, sendo /tmp/localstack/data o caminho padrão. Esses arquivos podem ser sincronizados com o hospedeiro através de entradas na tag volumes do docker-compose.yml. O conteúdo à esquerda do : é o caminho no hospedeiro e à direita, no container. Qualquer alteração em um dos lados é replicada no outro, porém, quando o container é destruído, o volume no hospedeiro permanece.

- ./localstack/data:/tmp/localstack/data

No caso do S3, a persistência funciona gravando as chamadas para a API em um arquivo JSON recorded_api_calls.json no DATA_DIR. A seguir, vemos o conteúdo das primeiras três linhas desse arquivo após executar os comandos deste artigo, sendo a primeira chamada para criar o bucket, a segunda para adicionar a regra ACL e a terceira para adicionar as notificações.

{"a": "s3", "m": "PUT", "p": "/mybucket", "d": "", "h": {"host": "localhost", "Accept-Encoding": "identity", "User-Agent": "aws-cli/1.18.35 Python/3.8.2 Linux/4.15.0-91-generic botocore/1.15.35", "X-Amz-Date": "20200411T020653Z", "X-Amz-Content-SHA256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "Authorization": "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20200411/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=a249cdba3fe3cfa6a594b3747d59c6daaed7ca65a9a14ebafd57a4810ccaa9af", "Content-Length": "0", "X-Forwarded-For": "127.0.0.1, 0.0.0.0:4572", "content-type": "binary/octet-stream", "Connection": "close"}, "rd": "PENyZWF0ZUJ1Y2tldFJlc3BvbnNlIHhtbG5zPSJodHRwOi8vczMuYW1hem9uYXdzLmNvbS9kb2MvMjAwNi0wMy0wMSI+PENyZWF0ZUJ1Y2tldFJlc3BvbnNlPjxCdWNrZXQ+bXlidWNrZXQ8L0J1Y2tldD48L0NyZWF0ZUJ1Y2tldFJlc3BvbnNlPjwvQ3JlYXRlQnVja2V0UmVzcG9uc2U+"}
{"a": "s3", "m": "PUT", "p": "/mybucket?acl", "d": "", "h": {"host": "localhost", "Accept-Encoding": "identity", "x-amz-acl": "public-read", "User-Agent": "aws-cli/1.18.35 Python/3.8.2 Linux/4.15.0-91-generic botocore/1.15.35", "X-Amz-Date": "20200411T020654Z", "X-Amz-Content-SHA256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "Authorization": "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20200411/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-acl;x-amz-content-sha256;x-amz-date, Signature=0e7447a0c63ae32b962cf20f42a5926784f1cc0581f187c08e5fdea80046648b", "Content-Length": "0", "X-Forwarded-For": "127.0.0.1, 0.0.0.0:4572", "content-type": "binary/octet-stream", "Connection": "close"}, "rd": ""}
{"a": "s3", "m": "PUT", "p": "/mybucket?notification", "d": "PE5vdGlmaWNhdGlvbkNvbmZpZ3VyYXRpb24geG1sbnM9Imh0dHA6Ly9zMy5hbWF6b25hd3MuY29tL2RvYy8yMDA2LTAzLTAxLyI+PFF1ZXVlQ29uZmlndXJhdGlvbj48UXVldWU+YXJuOmF3czpzcXM6dXMtZWFzdC0xOjAwMDAwMDAwMDAwMDpteXF1ZXVlPC9RdWV1ZT48RXZlbnQ+czM6T2JqZWN0Q3JlYXRlZDoqPC9FdmVudD48L1F1ZXVlQ29uZmlndXJhdGlvbj48L05vdGlmaWNhdGlvbkNvbmZpZ3VyYXRpb24+", "h": {"host": "localhost", "Accept-Encoding": "identity", "User-Agent": "aws-cli/1.18.35 Python/3.8.2 Linux/4.15.0-91-generic botocore/1.15.35", "X-Amz-Date": "20200411T020656Z", "X-Amz-Content-SHA256": "f74b4309e6e403e3027b1eff87b34f56cf696f4338e628a964985db13b6fe189", "Authorization": "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20200411/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=1a743b0573c0766272e98f44639b6828a52d93ba7988a472f7a1d6609d96d0a9", "Content-Length": "234", "X-Forwarded-For": "127.0.0.1, 0.0.0.0:4572", "content-type": "binary/octet-stream"}, "rd": ""}

Caso o container seja recriado, as chamadas desse arquivo são reaplicadas. Em chamadas para upload de arquivos no bucket, o conteúdo dos objetos é armazenado no formato binário, portanto, eu evito utilizar arquivos grandes durante o desenvolvimento.

Infelizmente, até o momento a persistência de dados do Localstack apenas funciona para os serviços Kinesis, DynamoDB, Elasticsearch, S3. Sendo assim, a fila do SQS teria que ser recriada junto com o container.

A solução que encontrei para esta questão foi utilizar um script de entrada do docker com o comando para criar a fila. No Localstack, os scripts em /docker-entrypoint-initaws.d são executados quando o container é criado, portanto, basta adicioná-lo como uma nova entrada de volume.

...
- ./localstack/docker-entrypoint.sh:/docker-entrypoint-initaws.d/docker-entrypoint.sh

A restauração do conteúdo em recorded_api_calls.json não gera registros na fila, uma vez que a execução do script de entrada é realizada posteriormente.

Extra

Para automatizar a configuração inicial criei um script com todos os comandos deste artigo para ser executado dentro do container. Maiores detalhes no repositório https://github.com/guizoxxv/localstack-test.

--

--

Gustavo Oliveira
Gustavo Oliveira

No responses yet