├── .circleci
└── config.yml
├── .eslintrc.json
├── .gitignore
├── Makefile
├── README.md
├── demo
├── lambdas
│ ├── app.js
│ ├── processing.py
│ └── worker.rb
└── web
│ ├── architecture.png
│ └── index.html
├── package-lock.json
├── package.json
└── serverless.yml
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | orbs:
4 | localstack: localstack/platform@2.0.0
5 |
6 | jobs:
7 | localstack-test:
8 | executor: localstack/default
9 | environment:
10 | CI_PROJECT: ci-demo-1
11 | DNS_ADDRESS: "0"
12 | steps:
13 | - checkout
14 | - run: docker pull localstack/localstack
15 | - localstack/startup
16 | - run:
17 | name: Deploy app and run test request
18 | command: |
19 | docker logs localstack-main
20 | awslocal s3 mb s3://test123
21 | awslocal s3 ls
22 | make install
23 | make deploy
24 | make web &
25 | make send-request
26 | code=$?
27 | docker logs localstack-main
28 | exit $code
29 |
30 | workflows:
31 | localstack-test:
32 | jobs:
33 | - localstack-test
34 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es6": true,
4 | "node": true,
5 | "mocha": true
6 | },
7 | "extends": "eslint:recommended",
8 | "parserOptions": {
9 | "ecmaVersion": 2018
10 | },
11 | "rules": {
12 | "arrow-parens": [
13 | "error",
14 | "always"
15 | ],
16 | "max-len": [
17 | "error",
18 | {
19 | "code": 120
20 | }
21 | ],
22 | "indent": [
23 | 2,
24 | 4,
25 | {
26 | "SwitchCase": 1
27 | }
28 | ],
29 | "brace-style": "error",
30 | "quotes": [
31 | "error",
32 | "single"
33 | ],
34 | "semi": [
35 | "error",
36 | "always"
37 | ],
38 | "no-var": "error"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | .serverless/
3 | node_modules
4 | .idea/
5 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | export AWS_ACCESS_KEY_ID ?= test
2 | export AWS_SECRET_ACCESS_KEY ?= test
3 | export AWS_DEFAULT_REGION = us-east-1
4 |
5 | usage: ## Show this help
6 | @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
7 |
8 | install: ## Install dependencies
9 | npm install
10 | which serverless || npm install -g serverless
11 | which localstack || pip install localstack
12 |
13 | deploy: ## Deploy the app
14 | @make install; \
15 | echo "Deploying Serverless app to local environment"; \
16 | SLS_DEBUG=1 serverless deploy --stage local
17 |
18 | send-request: ## Send a test request to the deployed application
19 | @which jq || (echo "jq was not found. Please install it (https://jqlang.github.io/jq/download/) and try again." && exit 1)
20 | @echo Looking up API ID from deployed API Gateway REST APIs ...; \
21 | apiId=$$(awslocal apigateway get-rest-apis --output json | jq -r '.items[] | select(.name="local-localstack-demo") | .id'); \
22 | echo Sending request to API Gateway REST APIs ID "$$apiId" ...; \
23 | requestID=$$(curl -s -d '{}' http://$$apiId.execute-api.localhost.localstack.cloud:4566/local/requests | jq -r .requestID); \
24 | echo "Received request ID '$$requestID'"; \
25 | for i in 1 2 3 4 5 6 7 8 9 10; do echo "Polling for processing result to appear in s3://archive-bucket/..."; awslocal s3 ls s3://archive-bucket/ | grep $$requestID && exit; sleep 3; done
26 |
27 | lint: ## Run code linter
28 | @npm run lint
29 | @flake8 demo
30 |
31 | .PHONY: usage install deploy send-request lint
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/whummer/localstack-demo)
2 |
3 | # LocalStack Demo
4 |
5 | Simple demo application deployed using LocalStack, developed using the Serverless framework.
6 |
7 | The sample app illustrates a typical Web application scenario with asynchronous request processing happening in the background, all running locally inside LocalStack. The figure below outlines the application architecture with the different components and services involved in processing the requests.
8 |
9 |
10 |
11 | ## Prerequisites
12 |
13 | * LocalStack
14 | * Docker
15 | * Node.js / `yarn`
16 | * `make`
17 | * (optional) jq
18 |
19 | Note: Please make sure to pull and start the `latest` LocalStack Docker image. At the time of writing (2023-02-01), the demo requires some features that were only recently added to LocalStack and are not part of a tagged release version yet.
20 |
21 | ## Running LocalStack
22 |
23 | Use the `localstack` CLI command to get started:
24 | ```
25 | localstack start
26 | ```
27 |
28 | ## Installing dependencies & running the application
29 |
30 | To install the dependencies, deploy and start the application locally in LocalStack:
31 | ```
32 | make deploy
33 | ```
34 |
35 | ## Testing
36 |
37 | After starting the app, open this URL in your browser: http://localhost:4566/archive-bucket/index.html
38 |
39 | * Enable the option "Auto-Refresh" to continuously poll for new results
40 | * Click the button "Create new request" to send a new request to the backend API
41 | * The new request will go through the phases `QUEUED->PROCESSING->FINISHED` as the request is being handled by the backend services (Lambda functions, Step Functions state machine)
42 |
43 | If you have the [`awslocal`](https://github.com/localstack/awscli-local) command line installed, you can browse the contents of the local S3 bucket via:
44 | ```
45 | awslocal s3 ls s3://archive-bucket/
46 | ```
47 |
48 | ## License
49 |
50 | This code is available under the Apache 2.0 license.
51 |
--------------------------------------------------------------------------------
/demo/lambdas/app.js:
--------------------------------------------------------------------------------
1 | const uuidv4 = require('uuid/v4');
2 | const AWS = require('aws-sdk');
3 |
4 | const AWS_ENDPOINT_URL = process.env.AWS_ENDPOINT_URL;
5 | if (AWS_ENDPOINT_URL) {
6 | process.env.AWS_SECRET_ACCESS_KEY = 'test';
7 | process.env.AWS_ACCESS_KEY_ID = 'test';
8 | }
9 |
10 | const DYNAMODB_TABLE = 'appRequests';
11 | const QUEUE_NAME = 'requestQueue';
12 | const CLIENT_CONFIG = AWS_ENDPOINT_URL ? {endpoint: AWS_ENDPOINT_URL} : {};
13 |
14 | const connectSQS = () => new AWS.SQS(CLIENT_CONFIG);
15 | const connectDynamoDB = () => new AWS.DynamoDB(CLIENT_CONFIG);
16 |
17 | const shortUid = () => uuidv4().substring(0, 8);
18 |
19 | const headers = {
20 | 'content-type': 'application/json',
21 | 'Access-Control-Allow-Headers' : 'Content-Type',
22 | 'Access-Control-Allow-Origin': '*',
23 | 'Access-Control-Allow-Methods': 'OPTIONS,POST,GET'
24 | };
25 |
26 | const handleRequest = async (event) => {
27 | if (event.path === '/requests' && event.httpMethod === 'POST') {
28 | return startNewRequest(event);
29 | } else if (event.path === '/requests' && event.httpMethod === 'GET') {
30 | return listRequests(event);
31 | } else {
32 | return {statusCode: 404, headers, body: {}};
33 | }
34 | };
35 |
36 | const startNewRequest = async () => {
37 | // put message onto SQS queue
38 | const sqs = connectSQS();
39 | const requestID = shortUid();
40 | const message = {'requestID': requestID};
41 | const queueUrl = (await sqs.getQueueUrl({QueueName: QUEUE_NAME}).promise()).QueueUrl;
42 | let params = {
43 | MessageBody: JSON.stringify(message),
44 | QueueUrl: queueUrl
45 | };
46 | await sqs.sendMessage(params).promise();
47 |
48 | // set status in DynamoDB to QUEUED
49 | const dynamodb = connectDynamoDB();
50 | const status = 'QUEUED';
51 | params = {
52 | TableName: DYNAMODB_TABLE,
53 | Item: {
54 | id: {
55 | S: shortUid()
56 | },
57 | requestID: {
58 | S: requestID
59 | },
60 | timestamp: {
61 | N: '' + Date.now()
62 | },
63 | status: {
64 | S: status
65 | }
66 | }
67 | };
68 | await dynamodb.putItem(params).promise();
69 |
70 | const body = JSON.stringify({
71 | requestID,
72 | status
73 | });
74 | return {
75 | statusCode: 200,
76 | headers,
77 | body
78 | };
79 | };
80 |
81 | const listRequests = async () => {
82 | const dynamodb = connectDynamoDB();
83 | const params = {
84 | TableName: DYNAMODB_TABLE,
85 | };
86 | const scanResult = await dynamodb.scan(params).promise();
87 | const items = scanResult['Items'].map((x) => {
88 | Object.keys(x).forEach((attr) => {
89 | if ('N' in x[attr]) x[attr] = parseFloat(x[attr].N);
90 | else if ('S' in x[attr]) x[attr] = x[attr].S;
91 | else x[attr] = x[attr][Object.keys(x[attr])[0]];
92 | });
93 | return x;
94 | });
95 | const result = {
96 | statusCode: 200,
97 | headers,
98 | body: JSON.stringify({result: items})
99 | };
100 | return result;
101 | };
102 |
103 | module.exports = {
104 | handleRequest
105 | };
106 |
--------------------------------------------------------------------------------
/demo/lambdas/processing.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import os
3 | import time
4 | import uuid
5 |
6 | import boto3
7 |
8 | AWS_ENDPOINT_URL = os.environ.get("AWS_ENDPOINT_URL")
9 |
10 | DYNAMODB_TABLE = 'appRequests'
11 | S3_BUCKET = os.environ.get('ARCHIVE_BUCKET') or 'archive-bucket'
12 |
13 |
14 | def handle_request(event, context=None):
15 | # simulate queueing delay
16 | time.sleep(5)
17 | print('handle_request', event)
18 | # set request status to PROCESSING
19 | status = 'PROCESSING'
20 | set_status(event['requestID'], status)
21 | # simulate processing delay
22 | time.sleep(4)
23 | return {
24 | 'requestID': event['requestID'],
25 | 'status': status
26 | }
27 |
28 |
29 | def archive_result(event, context=None):
30 | print('archive_result', event)
31 | requestID = event['requestID']
32 | # put result onto S3
33 | s3 = get_client('s3')
34 | s3.put_object(
35 | Bucket=S3_BUCKET,
36 | Key=f'{requestID}/result.txt',
37 | Body=f'Archive result for request {requestID}'
38 | )
39 | # simulate processing delay
40 | time.sleep(3)
41 | # set request status to FINISHED
42 | set_status(requestID, 'FINISHED')
43 |
44 |
45 | def get_client(resource):
46 | kwargs = {"endpoint_url": AWS_ENDPOINT_URL} if AWS_ENDPOINT_URL else {}
47 | return boto3.client(resource, **kwargs)
48 |
49 |
50 | def set_status(requestID, status):
51 | dynamodb = get_client('dynamodb')
52 | item = {
53 | 'id': {'S': short_uid()},
54 | 'requestID': {'S': requestID},
55 | 'timestamp': {'N': str(now_utc())},
56 | 'status': {'S': status}
57 | }
58 | dynamodb.put_item(TableName=DYNAMODB_TABLE, Item=item)
59 |
60 |
61 | def now_utc():
62 | diff = datetime.datetime.utcnow() - datetime.datetime(1970, 1, 1)
63 | return int(diff.total_seconds() * 1000.0)
64 |
65 |
66 | def short_uid():
67 | return str(uuid.uuid4())[0:8]
68 |
--------------------------------------------------------------------------------
/demo/lambdas/worker.rb:
--------------------------------------------------------------------------------
1 | require 'aws-sdk-states'
2 |
3 | $aws_endpoint_url = ENV['AWS_ENDPOINT_URL']
4 | if $aws_endpoint_url
5 | ENV['AWS_SECRET_ACCESS_KEY'] = 'test'
6 | ENV['AWS_ACCESS_KEY_ID'] = 'test'
7 | end
8 | $state_machine_arn = ENV['STATE_MACHINE_ARN']
9 |
10 | def triggerProcessing(event:, context:)
11 | if $aws_endpoint_url
12 | client = Aws::States::Client.new(:endpoint => $aws_endpoint_url)
13 | else
14 | client = Aws::States::Client.new()
15 | end
16 |
17 | records = event['Records']
18 | for rec in records do
19 | result = client.start_execution({
20 | state_machine_arn: $state_machine_arn,
21 | input: rec['body']
22 | })
23 | end
24 | {}
25 | end
26 |
--------------------------------------------------------------------------------
/demo/web/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localstack/localstack-demo/e58fed5e09085a6e2f92d3c028cbd923f2a4bde5/demo/web/architecture.png
--------------------------------------------------------------------------------
/demo/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
127 | This sample app illustrates a typical Web application scenario with asynchronous request processing happening in the background, all running locally inside LocalStack. 128 |
129 | The end-to-end process is illustrated in the figure on the right. 130 |
131 | Here's how the app works: 132 |
133 |143 | (Note that all resources will be deployed to the local "us-east-1" region.) 144 |
145 | The full source code of this sample is available in this Github repo: https://github.com/localstack/localstack-demo 146 |
147 |