diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6de9b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.terraform/ +*.zip +output.txt +terraform.tfstate +terraform.tfstate.backup diff --git a/README.md b/README.md index 4e34f2a..67c32f0 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,5 @@ The code from some of my blog posts. - [Profile PHP applications with xhgui and xhprof on docker](docker-xhprof-xhgui) - [Debug PHP applications running on docker with vscode](docker-vscode-php-xdebug) - [Backup docker volumes with borg](docker-volume-borg-backup) +- [A guide to localstack (part 1) - How to mock Amazon web services in local](localstack-part-1) +- [A guide to localstack (part 2) - deploying resources with Terraform](localstack-part-2) diff --git a/localstack-part-1/README.md b/localstack-part-1/README.md new file mode 100644 index 0000000..26f25e1 --- /dev/null +++ b/localstack-part-1/README.md @@ -0,0 +1,58 @@ +Replicate AWS in local with localstack +====================================== + +An example on how to use localstack to mock AWS services. The following instructions focus on how to run localstack and deploy: +* a dynamodb table +* a lambda reading data and putting data to this table + +Usage +----- + +Run + +```bash +docker-compose up -d +docker-compose logs -f localstack +``` + +Wait for set up to be done, then create a dynamodb table: + +```bash +aws dynamodb create-table \ + --endpoint-url http://localhost:4569 \ + --table-name table_1 \ + --attribute-definitions AttributeName=id,AttributeType=S \ + --key-schema AttributeName=id,KeyType=HASH \ + --provisioned-throughput ReadCapacityUnits=20,WriteCapacityUnits=20 +``` + +To check if the table was properly created, run: + +```bash +aws dynamodb list-tables --endpoint-url http://localhost:4569 +``` + +Create and deploy the lambda function: + +```bash +cd lambda +zip -r ../lambda.zip . +cd .. +aws lambda create-function \ + --function-name counter \ + --runtime nodejs8.10 \ + --role fake_role \ + --handler main.handler \ + --endpoint-url http://localhost:4574 \ + --zip-file fileb://$PWD/lambda.zip +``` + +Invoke the function multiple times and scan the table to see new items and their counters being incremented: + +``bash +aws lambda invoke --function-name counter --endpoint-url=http://localhost:4574 --payload '{"id": "test"}' output.txt +aws dynamodb scan --endpoint-url http://localhost:4569 --table-name table_1 + +aws lambda invoke --function-name counter --endpoint-url=http://localhost:4574 --payload '{"id": "test2"}' output.txt +aws dynamodb scan --endpoint-url http://localhost:4569 --table-name table_1 +``` \ No newline at end of file diff --git a/localstack-part-1/docker-compose.yml b/localstack-part-1/docker-compose.yml new file mode 100644 index 0000000..85df016 --- /dev/null +++ b/localstack-part-1/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3.3' + +networks: + + default: + external: + name: localstack-tutorial + +volumes: + + localstack: + +services: + + localstack: + image: localstack/localstack:0.9.0 + ports: + - 8080:8080 # webui + - 4569:4569 # dynamodb + - 4574:4574 # lamba + environment: + - DATA_DIR=/tmp/localstack/data + - DEBUG=1 + - DEFAULT_REGION=ap-southeast-2 + - DOCKER_HOST=unix:///var/run/docker.sock + - LAMBDA_EXECUTOR=docker-reuse + - PORT_WEB_UI=8080 + - SERVICES=lambda,dynamodb + - LAMBDA_DOCKER_NETWORK=localstack-tutorial + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - localstack:/tmp/localstack/data diff --git a/localstack-part-1/lambda/main.js b/localstack-part-1/lambda/main.js new file mode 100644 index 0000000..05cb7e0 --- /dev/null +++ b/localstack-part-1/lambda/main.js @@ -0,0 +1,66 @@ +'use strict'; + +const AWS = require('aws-sdk'); + +AWS.config.update({ + region: 'ap-southeast-2', + endpoint: 'http://localstack:4569' +}); + +class DynamoDBService { + constructor() { + this.docClient = new AWS.DynamoDB.DocumentClient({ apiVersion: '2012-08-10' }); + } + + async increment(id) { + return new Promise(async (resolve, reject) => { + try { + const count = await this.getCount(id); + var params = { + TableName: 'table_1', + Item: { + count: count + 1, + id: id + } + }; + + this.docClient.put(params, function(err, data) { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + } catch (err) { + reject(err); + } + }); + } + + async getCount(id) { + return new Promise(async (resolve, reject) => { + var params = { + TableName: 'table_1', + Key: {id} + }; + + this.docClient.get(params, function(err, data) { + if (err) { + reject(err); + } else { + resolve(data['Item'] ? data['Item']['count'] : 0); + } + }); + }); + } +} + +exports.handler = async (event, context, callback) => { + try { + const dynamoDBService = new DynamoDBService(); + await dynamoDBService.increment(event.id); + callback(null, {}); + } catch (error) { + callback(error); + } +} \ No newline at end of file diff --git a/localstack-part-2/README.md b/localstack-part-2/README.md new file mode 100644 index 0000000..01f0d8a --- /dev/null +++ b/localstack-part-2/README.md @@ -0,0 +1,42 @@ +Deploy AWS resources in localstack with Terraform +================================================= + +An example on how to use Terraform to deploy localstack resources that mock AWS services. The following instructions focus on how to deploy: +* a dynamodb table +* a lambda reading data and putting data to this table + +Usage +----- + +Run + +```bash +docker-compose up -d +docker-compose logs -f localstack +``` + +Create the lambda: + +```bash +cd lambda +zip -r ../lambda.zip . +cd .. +``` + +Wait for set up to be done, then apply the Terraform configuration: + +```bash +terraform init +terraform plan +terraform apply --auto-approve +``` + +Invoke the lambda multiple times and scan the table to see new items and their counters being incremented: + +``bash +aws lambda invoke --function-name counter --endpoint-url=http://localhost:4574 --payload '{"id": "test"}' output.txt +aws dynamodb scan --endpoint-url http://localhost:4569 --table-name table_1 + +aws lambda invoke --function-name counter --endpoint-url=http://localhost:4574 --payload '{"id": "test2"}' output.txt +aws dynamodb scan --endpoint-url http://localhost:4569 --table-name table_1 +``` \ No newline at end of file diff --git a/localstack-part-2/docker-compose.yml b/localstack-part-2/docker-compose.yml new file mode 100644 index 0000000..85df016 --- /dev/null +++ b/localstack-part-2/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3.3' + +networks: + + default: + external: + name: localstack-tutorial + +volumes: + + localstack: + +services: + + localstack: + image: localstack/localstack:0.9.0 + ports: + - 8080:8080 # webui + - 4569:4569 # dynamodb + - 4574:4574 # lamba + environment: + - DATA_DIR=/tmp/localstack/data + - DEBUG=1 + - DEFAULT_REGION=ap-southeast-2 + - DOCKER_HOST=unix:///var/run/docker.sock + - LAMBDA_EXECUTOR=docker-reuse + - PORT_WEB_UI=8080 + - SERVICES=lambda,dynamodb + - LAMBDA_DOCKER_NETWORK=localstack-tutorial + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - localstack:/tmp/localstack/data diff --git a/localstack-part-2/lambda/main.js b/localstack-part-2/lambda/main.js new file mode 100644 index 0000000..05cb7e0 --- /dev/null +++ b/localstack-part-2/lambda/main.js @@ -0,0 +1,66 @@ +'use strict'; + +const AWS = require('aws-sdk'); + +AWS.config.update({ + region: 'ap-southeast-2', + endpoint: 'http://localstack:4569' +}); + +class DynamoDBService { + constructor() { + this.docClient = new AWS.DynamoDB.DocumentClient({ apiVersion: '2012-08-10' }); + } + + async increment(id) { + return new Promise(async (resolve, reject) => { + try { + const count = await this.getCount(id); + var params = { + TableName: 'table_1', + Item: { + count: count + 1, + id: id + } + }; + + this.docClient.put(params, function(err, data) { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + } catch (err) { + reject(err); + } + }); + } + + async getCount(id) { + return new Promise(async (resolve, reject) => { + var params = { + TableName: 'table_1', + Key: {id} + }; + + this.docClient.get(params, function(err, data) { + if (err) { + reject(err); + } else { + resolve(data['Item'] ? data['Item']['count'] : 0); + } + }); + }); + } +} + +exports.handler = async (event, context, callback) => { + try { + const dynamoDBService = new DynamoDBService(); + await dynamoDBService.increment(event.id); + callback(null, {}); + } catch (error) { + callback(error); + } +} \ No newline at end of file diff --git a/localstack-part-2/localstack.tf b/localstack-part-2/localstack.tf new file mode 100644 index 0000000..7c3c23e --- /dev/null +++ b/localstack-part-2/localstack.tf @@ -0,0 +1,42 @@ +provider "aws" { + region = "ap-southeast-2" + access_key = "fake" + secret_key = "fake" + + skip_credentials_validation = true + skip_requesting_account_id = true + + endpoints { + dynamodb = "http://localhost:4569" + lambda = "http://localhost:4574" + } +} + +resource "aws_dynamodb_table" "table_1" { + name = "table_1" + read_capacity = "20" + write_capacity = "20" + hash_key = "id" + + attribute { + name = "id" + type = "S" + } +} + +resource "aws_lambda_function" "counter" { + function_name = "counter" + filename = "lambda.zip" + role = "fake_role" + handler = "main.handler" + runtime = "nodejs8.10" + timeout = 30 + + lifecycle { + ignore_changes = [ + "environment", + "memory_size", + "role", + ] + } +} \ No newline at end of file