Containerizing Serverless APIs

Use Docker, LocalStack and the Serverless Framework for Local Development

Van Huynh
ITNEXT

--

Branding credits — Docker, AWS, Serverless and LocalStack.

While working on the Contacts app to consume the Contacts API from the above article I wrote, I found it cumbersome to have multiple projects and terminals open just to run the app.

I would run a terminal for the DynamoDb jar file, another for the API in serverless offline mode and one more terminal for the actual front end app to build and run.

To help me focus on the development of the front end app, I decided to run DynamoDb and my serverless API in Docker containers. This way I could keep the containers running in the background, have it persist data, and easily tear it down or reset it whenever I felt like it.

TL;DR

Clone the contacts_api project from GitHub and inspect the repository.

Run the docker-compose.yml file with, docker-compose up -d, which should create two containers and start them detached in the background. One container should be using the LocalStack image with only the DynamoDb service started and the other is a Node container running the contacts_api code with npm start.

Once both containers are up and running, execute the seed command to seed some data for the API. The seed data is a JSON file in the contacts_api project.

# Do this each time docker-compose down, 
# then docker-compose up is run.
$ docker exec -it contacts_api npm run seed# Test the API$ curl -i localhost:3000/contacts# Should see an array of contacts

Setup

  • Contacts API project — The project to build a container for.
  • Docker — Download and install Docker for Developers for your OS.
  • LocalStack Docker image — Use this as the image to host the DynamoDb instance. Make sure to use the localstack/localstack image.

From the contacts_api project, create a Dockerfile at the root and add the following instructions:

# File: ./DockerfileFROM node:8.12.0# Copy the package.json and package.lock.json file to the image
WORKDIR /usr/src/app
COPY package*.json ./
# Run `npm install` to install the dependencies in the
# package.json file
RUN npm install
# Copy the contents of the project to the image
COPY . .
# This is the default port the serverless framework will
# listen on when it starts
EXPOSE 3000
# These are just to remind me that these are the
# environment variables this project uses
# ENV AWS_ENDPOINT='http://localhost:8000'
# ENV AWS_REGION='us-west-1'
# ENV AWS_ACCESS_KEY_ID='from-dockerfile-fake-access-key'
# ENV AWS_SECRET_ACCESS_KEY='from-dockerfile-fake-secret-key'
# Run 'npm start' when the container starts.
# This will start serverless, in offline mode, just like
# running the project from the terminal locally
CMD [ "npm", "start" ]

Next add a .dockerignore file to ignore files and folders that aren’t needed for the image.

# File: ./.dockerignore.vscode/
.serverless/
coverage/
node_modules/
.env

With the Dockerfile and .dockerignore files created, add a docker-compose.yml file to build and run the containers.

Start by creating a docker-compose.yml file at the same location as the Dockerfile, and add the following YAML code to it:

# File: ./docker-compose.ymlversion: '3'services:
api:
# Look for a Dockerfile at the the root
build: .
# <name> can be the project name, username, anything...
image: <name>/contacts_api
depends_on:
- localstack

# Mapping port 3000 to the container's port 3000 since
# running `npm start` will start serverless offline on
# that port
ports:
- '3000:3000'

container_name: contacts_api
# These are the environment variables that are used by the api
environment:
# The name of the localstack container is host for dynamodb
AWS_ENDPOINT: 'http://dynamodb_localstack:8000'
AWS_REGION: 'us-west-2'
# leaving these blank will try and read from the terminal
AWS_ACCESS_KEY_ID:
AWS_SECRET_ACCESS_KEY:
# Use the LocalStack image to run DynamoDb in a container
localstack:
image: localstack/localstack:latest
# Using port 8000 to be consistent with dynamodb local jar
# port 8080 is the localstack management portal
ports:

- '8000:8000'
- '8080:8080'
container_name: dynamodb_localstack

# Only interested in running dynamodb and having it
# persist data when the container is stopped
environment:
SERVICES: dynamodb:8000
DATA_DIR: '/tmp/localstack/data'

Build and run

Open a terminal window where the docker-compose.yml file is located and run:

# Terminal at ./contacts_api (where docker-compose.yml is located)
# the ordering of the print outs might be different
$ docker-compose up
# or to run in the background in detached mode:
# docker-compose up -d
# Successful build and run output should look like this.
# Note - I've edited the output for brevity:
Step 1/7 : FROM node:8.12.0
Step 2/7 : WORKDIR /usr/src/app
Step 3/7 : COPY package*.json ./
Step 4/7 : RUN npm install
...
...
# Dependencies from the package.json from the contacts_api
# project are installed...
added 887 packages from 585 contributors and audited 18846 packages in 21.944sStep 5/7 : COPY . .
Step 6/7 : EXPOSE 3000
Step 7/7 : CMD [ "npm", "start" ]
Successfully tagged <name>/contacts_api:latest# The localstack image was already pulled to my machineCreating dynamodb_localstack ... done
Creating contacts_api ... done
...
...
# LocalStack starting the admin portal on port 8080
# like we told it to
dynamodb_localstack | 2018-10-30T04:46:20:INFO:werkzeug:
* Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
# LocalStack starting the DynamoDb service on port 8000dynamodb_localstack | Starting mock DynamoDB (http port 8000)...
dynamodb_localstack | Ready.
...# This is the output from running `npm start` in the
# contacts_api container. It is the same output
# as running `npm start` from the project directly
contacts_api | > contacts-api@1.0.0 start /usr/src/app
contacts_api | > sls offline start
contacts_api |
contacts_api | Serverless: Starting Offline: dev/us-west-2.
contacts_api | Serverless: Routes for list:
contacts_api | Serverless: GET /contacts
contacts_api |
contacts_api | Serverless: Routes for get:
contacts_api | Serverless: GET /contact/{id}
contacts_api |
contacts_api | Serverless: Routes for add:
contacts_api | Serverless: POST /contact
contacts_api |
contacts_api | Serverless: Routes for update:
contacts_api | Serverless: PUT /contact/{id}
contacts_api |
contacts_api | Serverless: Routes for delete:
contacts_api | Serverless: DELETE /contact/{id}
contacts_api |
contacts_api | Serverless: Offline listening on http://0.0.0.0:3000

Before the API is usable, the contacts table in DynamoDb needs to be created and seed it with data. There is a create/seed script in the contacts_api project for doing this so let’s leverage that.

Open a new terminal window or detach/exit from the terminal that is attached to the docker process with (ctrl+p, ctrl+q) or ctrl+\ (the last one might not work) and run the following command:

# Execute the seed command against the contacts_api container$ docker exec -it contacts_api npm run seed# output from the 'seed' script in the project> contacts-api@1.0.0 seed /usr/src/app
> node ./seed/runner.js
>> Checking if 'contacts' table exists
>> Creating 'contacts' table
>> Seeding data
>> Done!

As a sanity check, attempt to hit one of the endpoints. If there are no error messages, then the API is running as expected.

$ curl -i localhost:3000/contactsHTTP/1.1 200 OK
content-type: application/json; charset=utf-8
cache-control: no-cache
content-length: 411
accept-ranges: bytes
Date: Tue, 30 Oct 2018 05:18:34 GMT
Connection: keep-alive
[...array-of-seeded-contacts]

Note that seeding DynamoDb is required each time the containers are torn down and brought back up with docker-compose down and docker-compose up.

To stop and start the containers, simply run docker-compose stop and docker-compose start.

That’s it! Now the containers are running in the background, I can focus on developing apps that use the API.

Article source: https://github.com/vanister/contacts_api

--

--

Writer for

Software developer, car enthusiast, gamer, mentor and lifelong learner. I love a good pair-programming session.