Setting up a AWS development environment with localstack and docker
December 01, 2018
I recently finished a task that involved setting a development environment to build a Lambda function that would interact with AWS services.
The Lambda would be evoked on S3 put events which would then stream and parse XML files to JSON before inserting the result into a DynamoDB table.
The first pain point I had was understanding how to interface with AWS services locally which involved setting up a AWS environment locally with localstack.
Localstack provides an easy-to-use test/mocking framework for developing applications on AWS. It provides emulation of the most popular AWS services locally which can be accessed with the same SDK’s provided by AWS such as the Node aws-sdk.
Getting all the services running together was the next challenge and Docker was the next step for creating a self contained environment.
I wanted it to be fairly easy for another developer to spin up and work on the function without having to ask me for credentials or how to install Localstack.
The following will go over the configuration files I used to make this work with Localstack and Docker for a faily harmonious development environment for AWS services.
Setting up the Docker container
Set up a boilerplate Node project using NPM or Yarn then create a Dockerfile.
Run through the Yarn prompts then
FROM node:8.4.0 RUN apt-get update # Official AWS documentation recommends using python3 and associated tooling.That doesn't work, or at least it does not work as easily as advertised. RUN apt-get install python-dev python-pip -y # The awsebcli has a dependency issue and this resolves it RUN easy_install --upgrade six RUN pip install awscli WORKDIR /usr/src/app COPY package.json yarn.lock /usr/src/app/ RUN yarn COPY . /usr/src/app CMD ["yarn", "dev"]
A Dockerfile is used to build images from the instructions laid out using commands where those commands are run sequentially from this file.
In our Dockerfile we start by installing Node and the development pain free version of python and pip to avoid errors when working with the aws-cli. Once the aws-cli is installed, the working directory is set where the Node dependencies are added and the project script commands can be run.
We’ll define the same working directory path in the
docker-compose.yml file next.
version: '3' services: lambda-parser: build: . volumes: - .:/usr/src/app - /usr/src/app/node_modules environment: - AWS_ACCESS_KEY_ID=foobar - AWS_SECRET_ACCESS_KEY=foobar - AWS_DEFAULT_REGION=us-east-1 - AWS_S3_ENDPOINT=http://lambda-parser-aws:4572 - AWS_S3_PATH_STYLE=true - AWS_DDB_ENDPOINT=http://lambda-parser-aws:4569 depends_on: - lambda-parser-aws command: sh -c "yarn && yarn dev" lambda-parser-aws: image: localstack/localstack:0.8.7 ports: - "5000:8080" - "4572:4572" - "4569:4569" expose: - "4572" - "4569" environment: - SERVICES=s3,dynamodb - HOSTNAME=lambda-parser-aws
The service definition contains a configuration that is applied to each container started for that service.
We will define two service configurations, the lambda-parser and the localstack service as lambda-parser-aws.
The lambda-parser service represents the Node.js project that the lambda will be developed in. It will interact with the localstack container over a default network created automatically by docker-compose.
The lambda-parser-aws service will expose the localstack instance through the defined port 5000 and every other service we define on their respective ports listed here.
We’ll need to tell localstack to expose the S3 & DynamoDB services on ports 4572 and 4569.
If you’d like to add additional services such as SQS simply add them to the SERVICES=s3,dynamodb,sqs and expose the port defined from the localstack documentation.
.PHONY: up down reboot help ## Run the service and watch for changes up: docker-clean-images docker-compose up ## Shut down the service and any associated volume down: docker-compose down --volumes ## Start from scratch again reboot: down up docker-clean-images: docker image prune --force --filter "until=24h" docker-clean-volumes: docker volume prune --force docker-nuke: docker system prune --force --all ## Run a yarn command inside the container %: docker-compose exec ern-processor yarn $@
A Makefile is a special file, containing shell commands executed on the terminal.
In our Makefile we want to be able to use Docker Compose to spin up all of the services we defined in the
docker-compose.yml file which will also be in charge of running the Node script
yarn dev from the Dockerfile.
I think Makefiles are great for this type of problem because it gives you a single access point for interacting with your entire application stack.
make down will spin down the Docker containers cleaning up resources and
make reboot will restart all containers after spinning them down.
Setting up aws-cli shell scripts
const shell = require('shelljs'); // S3 shell.echo('Creating s3 bucket and uploading ingest...'); shell.exec('aws --endpoint-url=http://lamda-parsar-aws:4572 s3 mb s3://my-bucket'); shell.exec('aws --endpoint-url=http://lamda-parsar-aws:4572 s3 sync ingest s3://my-bucket --acl public-read'); shell.exec('aws --endpoint-url=http://lamda-parsar-aws:4572 s3api get-object-acl --bucket my-bucket --key my-xml-file.xml'); // Dynamodb shell.echo('Creating DynamoDB table...'); shell.exec('aws --endpoint-url=http://lambda-parsar-aws:4569 dynamodb create-table \ --table-name XmlToJson \ --attribute-definitions \ AttributeName=ID,AttributeType=S \ --key-schema AttributeName=ID,KeyType=HASH \ --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1'); shell.echo('Bootstrap complete');
At this point you might be wondering how we work with the AWS services on localstack, how do I actually create a Bucket and DynamoDB table.
You have a couple of options:
- write scripts that utilise the AWS Sdk to provision the services you need and seed them with data
- use the aws-cli to provision the services and seed them with data
shelljs npm package we are going to work with the second option. Essentially you’ll define terminal commands that will be executed in order using the aws-cli inside the lambda-parsar Docker container.
I’ve added a couple of example scripts that create an S3 bucket, upload some data and change the permissions on the created object to public. Thenwe create the DynamoDB table and define the key schema as ‘ID’ which will be the primary hash key used to look-up entities.
The purpose of this bootstrapping file is to simulate what your DevOps flow would look like when you actually create the services on AWS.
Doing it from within the Node project allowed me to quickly create the instances and configuration I needed to focus solely on the business logic of the Lambda itself, harmonious right?
From here you have most of the boilerplate needed to start developing with AWS services locally without needing to work with any live instances on the cloud meaning less headache on credential management and permissions.