I believe there are many people who have used AWS for their projects or are learning AWS. We know that with cloud services like AWS, it’s not very easy to connect to the cloud when developing locally, not to mention that the staging/production environment will have security considerations. So when we create a project, how do we build its local development environment to facilitate our local development and debugging? That’s right! Use localstack!

LocalStack - A fully functional local AWS cloud stack

LocalStack provides an easy-to-use test/mocking framework for developing Cloud applications.

Currently, the focus is primarily on supporting the AWS cloud stack.

How is it used?

Create an SNS service with LocalStack

Imagine we have a SpringBoot service that provides an interface to send an email to a user when the user submits a record, which we would normally do asynchronously by saving the database and sending the email. If we use AWS, we can call the SNS service after saving the database, publish an Event and wait for the downstream mail service to subscribe to the corresponding topic and then consume it.

So how do we create it? In the docker-compose.yml file, add localstack, SERVICES and specify the sns as follows

docker-compose.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
version: "3.7"

services:
  localstack:
    image: localstack/localstack:0.12.1
    networks:
      - app_net
    ports:
      - "4566:4566"
    environment:
      - SERVICES=sns
      - DEFAULT_REGION=ap-southeast-2
      - DEBUG=1
    volumes:
      - ./auto/create-localstack-topic:/docker-entrypoint-initaws.d/create-localstack-topic.sh

create-localstack-topic.sh

In addition, you can see that we have a script in the volume, which is the script to create the SNS. In fact, this command is aws cli, which can be found on its official website, we just need to replace aws with awslocal

1
2
3
4
5
6
#!/bin/bash

REGION=${DEFAULT_REGION:-ap-southeast-2}
TOPIC_NAME=demo-events-topic

awslocal sns create-topic --name=${TOPIC_NAME} --region "${REGION}"

Startup Log

When docker-compose up localstack is started, the SNS service is created in that container for us to use.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
sns_1            | Waiting for all LocalStack services to be ready
sns_1            | 2020-12-27 07:02:36,554 CRIT Supervisor is running as root.  Privileges were not dropped because no user is specified in the config file.  If you intend to run as root, you can set user=root in the config file to avoid this message.
sns_1            | 2020-12-27 07:02:36,559 INFO supervisord started with pid 15
sns_1            | 2020-12-27 07:02:37,566 INFO spawned: 'dashboard' with pid 21
sns_1            | 2020-12-27 07:02:37,571 INFO spawned: 'infra' with pid 22
sns_1            | 2020-12-27 07:02:37,577 INFO success: dashboard entered RUNNING state, process has stayed up for > than 0 seconds (startsecs)
sns_1            | 2020-12-27 07:02:37,577 INFO exited: dashboard (exit status 0; expected)
sns_1            | (. .venv/bin/activate; exec bin/localstack start --host)
sns_1            | 2020-12-27 07:02:38,591 INFO success: infra entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
sns_1            | LocalStack version: 0.12.1
sns_1            | Starting local dev environment. CTRL-C to quit.
sns_1            | 2020-12-27T07:02:39:DEBUG:bootstrap.py: Loading plugins - scope "services", module "localstack": <function register_localstack_plugins at 0x7f963f120f70>
sns_1            | Waiting for all LocalStack services to be ready
sns_1            | 2020-12-27T07:02:43:INFO:localstack.utils.analytics.profiler: Execution of "load_plugin_from_path" took 4333.9550495147705ms
sns_1            | 2020-12-27T07:02:43:INFO:localstack.utils.analytics.profiler: Execution of "load_plugins" took 4334.24186706543ms
sns_1            | Starting edge router (https port 4566)...
sns_1            | Starting mock SNS service on http port 4566 ...
sns_1            | 2020-12-27T07:02:45:INFO:localstack.utils.analytics.profiler: Execution of "prepare_environment" took 2061.4540576934814ms
sns_1            | 2020-12-27T07:02:45:INFO:localstack.multiserver: Starting multi API server process on port 59903
sns_1            | [2020-12-27 07:02:45 +0000] [23] [INFO] Running on https://0.0.0.0:4566 (CTRL + C to quit)
sns_1            | 2020-12-27T07:02:45:INFO:hypercorn.error: Running on https://0.0.0.0:4566 (CTRL + C to quit)
sns_1            | [2020-12-27 07:02:45 +0000] [23] [INFO] Running on http://0.0.0.0:59903 (CTRL + C to quit)
sns_1            | 2020-12-27T07:02:45:INFO:hypercorn.error: Running on http://0.0.0.0:59903 (CTRL + C to quit)
sns_1            | 2020-12-27 07:02:45,824:API:  * Running on http://0.0.0.0:57589/ (Press CTRL+C to quit)
sns_1            | Waiting for all LocalStack services to be ready
sns_1            | Ready.
sns_1            | 2020-12-27T07:02:50:INFO:localstack.utils.analytics.profiler: Execution of "start_api_services" took 5102.221965789795ms
sns_1            | /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initaws.d/create-localstack-topic.sh
sns_1            | {
sns_1            |     "TopicArn": "arn:aws:sns:ap-southeast-2:000000000000:demo-events-topic"
sns_1            | }

Calling

1
aws --endpoint-url=http://localhost:4566 sns publish --topic-arn arn:aws:sns:ap-southeast-2:000000000000:demo-events-topic --region ap-southeast-2 --message "Hello SNS"

Note that you need to override the endpoint-url when calling services in localstack locally with the aws command, otherwise the credentials will be used to call the services in the real environment.

Notes for use in SpringBoot

For use in SpringBoot or other codebases (such as node), you can create different SNSClients for different environments, taking care to override the endpoint for local environments.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Configuration
@Profile({"local", "docker"})
public class LocalSnsClientConfiguration {

    @Value("${aws.sns.endpoint}")
    private String awsSnsEndpoint;

    @Bean
    public SnsClient snsClient() {
        var clientBuilder = SnsClient.builder();
        if (!Strings.isNullOrEmpty(awsSnsEndpoint)) {
            clientBuilder.endpointOverride(URI.create(awsSnsEndpoint));
        }
        return clientBuilder.build();
    }
}

Start multiple services

The above is just an example of starting an SNS service. In practice, we will use multiple services in combination. For example, there will be an SQS service that subscribes to an SNS topic and then triggers a lambda to perform some tasks, so how can we implement these services locally to subscribe and trigger each other? In fact, you only need to start multiple services in a localstack and then execute some scripts to establish the relationship between them (the specific commands are the same as aws cli), as follows.

docker-compose.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
version: "3.7"

services:
  localstack:
    image: localstack/localstack:0.12.1
    privileged: true
    container_name: localstack
    networks:
      - app_net
    ports:
      - "4566:4566"
    environment:
      - SERVICES=sns,sqs,kms,cloudwatch,lambda
      - DEFAULT_REGION=ap-southeast-2
      - LAMBDA_EXECUTOR=docker-reuse
      - LAMBDA_REMOTE_DOCKER=false
      - LAMBDA_DOCKER_NETWORK=host
      - DEBUG=1
      - HOST_TMP_FOLDER=${TMPDIR}
      - DOCKER_HOST=unix:///var/run/docker.sock
      - LOCAL_CODE_PATH=${PWD}
    volumes:
      - ${TMPDIR:-/tmp/localstack}:/tmp/localstack
      - /var/run/docker.sock:/var/run/docker.sock
      - ./auto/create-localstack:/docker-entrypoint-initaws.d/create-localstack.sh
      - ./kms/kms_seed.yaml:/init/seed.yaml

networks:
  app_net:

kms_seed.yaml

I have started sns,sqs,kms,cloudwatch,lambda here. It is worth mentioning that in addition to overriding the endpoint-url for local access to sqs, sns, kms, etc., you need to specify a seed.yml to encrypt and decrypt it for local use with kms.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Keys:
  Symmetric:
    Aes:
      - Metadata:
          KeyId: 832ac356-3c82-4c4d-a3dc-7489da152197
        BackingKeys:
          - 2bdaead27fe7da2de47945d34cd6d79e36494e73802f3cd3869f1d2cb0b5d74c
Aliases:
  - AliasName: alias/testing
    TargetKeyId: 832ac356-3c82-4c4d-a3dc-7489da152197

Create script create-localstack.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash

QUEUE_NAME=demo-queue
TOPIC_NAME=demo-topic
FUNCTION_NAME=demo-function
APP_ENV=dev

awslocal sns create-topic --name=${TOPIC_NAME}
awslocal sqs create-queue --queue-name=${QUEUE_NAME}
awslocal sns subscribe \
    --topic-arn arn:aws:sns:ap-southeast-2:000000000000:${TOPIC_NAME} \
    --protocol sqs \
    --notification-endpoint http://localhost:4566/000000000000/${QUEUE_NAME}

awslocal lambda create-function \
    --code S3Bucket="__local__",S3Key="${LOCAL_CODE_PATH}" \
    --function-name ${FUNCTION_NAME} \
    --runtime nodejs12.x \
    --timeout 5 \
    --handler dist/index.handler \
    --role dev \
    --environment "{\"Variables\":{\"APP_ENV\":\"${APP_ENV}\"}}"

awslocal lambda create-event-source-mapping \
    --event-source-arn arn:aws:sqs:ap-southeast-2:000000000000:${QUEUE_NAME} \
    --function-name ${FUNCTION_NAME} \
    --enabled

Local Start

execution docker-compose up localstack

Now send an SNS message from the command line to trigger our Lambda execution.

1
aws --endpoint-url=http://localhost:4566 sns publish --topic-arn arn:aws:sns:ap-southeast-2:000000000000:demo-topic --region ap-southeast-2 --message "Hello SNS - SQS - Lambda"

Write a handler() method to Lambda’s index.ts.

1
2
3
4
5
6
require('./overwriteAwsLocalEndpoint'); //overwrite aws local endpoint,Please keep it here.
import { SQSEvent, SQSHandler } from 'aws-lambda';

export const handler: SQSHandler = (event: SQSEvent) => {
  console.log(JSON.stringify(event.Records));
}

Print the message body of the SQS.

1
2
3
4
localstack    | > START RequestId: ce5ae5ff-054d-16e0-dc62-71161118d3bd Version: $LATEST
localstack    | > 2020-12-27T09:32:56.373Z	ce5ae5ff-054d-16e0-dc62-71161118d3bd	INFO	[{"body":"{\"Type\": \"Notification\", \"MessageId\": \"04c12e03-66d0-474a-a60a-f0b3c2451456\", \"Token\": null, \"TopicArn\": \"arn:aws:sns:ap-southeast-2:000000000000:demo-topic\", \"Message\": \"Hello SNS - SQS - Lambda\", \"SubscribeURL\": null, \"Timestamp\": \"2020-12-27T09:32:52.202Z\", \"SignatureVersion\": \"1\", \"Signature\": \"EXAMPLEpH+..\", \"SigningCertURL\": \"https://sns.us-east-1.amazonaws.com/SimpleNotificationService-0000000000000000000000.pem\"}","receiptHandle":"exexifyylldwuznxlicibcanaqvcplpaeoztcdlltkzbsuvwiifvlyixrxwuzrmumlmkggofmiencdxilzoaluyreszdppsbycpxcowwvmeiieeplulkitfztfxzkjazucucauhuobpvlzdcnjdcmygqvbrouxkxoggcfryzqtibyquhikawczuif","md5OfBody":"d96df71c445e9282ed4c2fefbf4c8ca1","eventSourceARN":"arn:aws:sqs:ap-southeast-2:000000000000:demo-queue","eventSource":"aws:sqs","awsRegion":"ap-southeast-2","messageId":"a59f7c57-651b-54f6-70bd-a2933fa57099","attributes":{"SenderId":"AIDAIT2UOQQY3AUEKVGXU","SentTimestamp":"1609061572241","ApproximateReceiveCount":"1","ApproximateFirstReceiveTimestamp":"1609061572312"},"messageAttributes":{},"md5OfMessageAttributes":null,"sqs":true}]
localstack    | > END RequestId: ce5ae5ff-054d-16e0-dc62-71161118d3bd
localstack    | > REPORT RequestId: ce5ae5ff-054d-16e0-dc62-71161118d3bd	Init Duration: 3381.65 ms	Duration: 13.13 ms	Billed Duration: 100 ms	Memory Size: 1536 MB	Max Memory Used: 55 MB

About the use of Lambda in LocalStack

Creating a Lambda runtime environment locally is one of the more troublesome services in my opinion, and the following is the official configuration for Lambda creation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
STEPFUNCTIONS_LAMBDA_ENDPOINT: URL to use as the Lambda service endpoint in Step Functions. By default this is the LocalStack Lambda endpoint. Use default to select the original AWS Lambda endpoint.

LAMBDA_EXECUTOR: Method to use for executing Lambda functions. Possible values are:

	- local: run Lambda functions in a temporary directory on the local machine
	- docker: run each function invocation in a separate Docker container
	- docker-reuse: create one Docker container per function and reuse it across invocations
	For docker and docker-reuse, if LocalStack itself is started inside Docker, then the docker command needs to be available inside the container (usually requires to run the container in privileged mode). Default is docker, fallback to local if Docker is not available.

LAMBDA_REMOTE_DOCKER: determines whether Lambda code is copied or mounted into containers. Possible values are:

	- true (default): your Lambda function definitions will be passed to the container by copying the zip file (potentially slower). It allows for remote execution, where the host and the client are not on the same machine.
	- false: your Lambda function definitions will be passed to the container by mounting a volume (potentially faster). This requires to have the Docker client and the Docker host on the same machine.

LAMBDA_DOCKER_NETWORK: Optional Docker network for the container running your lambda function.

LAMBDA_DOCKER_DNS: Optional DNS server for the container running your lambda function.

LAMBDA_CONTAINER_REGISTRY: Use an alternative docker registry to pull lambda execution containers (default: lambci/lambda).

LAMBDA_REMOVE_CONTAINERS: Whether to remove containers after Lambdas finished executing (default: true).

FAQ

Why do I always get some client’s credentials error when I use SNS, KMS?

Because the endpoint-url required for the local environment is not overridden, refer to the explanation in this article

Why did the message to the SNS succeed but did not trigger Lambda?

qing check your creation script to make sure that your SQS is subscribed to the corresponding topic of the SNS and that the SQS has a Mapping that can trigger Lambda

This example code?

Fatezhang/aws-localstack-demo


Reference http://zhangjiaheng.cn/blog/20201227/%E4%BD%BF%E7%94%A8localstack-%E6%90%AD%E5%BB%BA-AWS-%E6%9C%AC%E5%9C%B0%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83/