Deploying to AWS or any other server environment can be a chore. Fortunately there are tools like Elastic Beanstalk and Docker to make life a little easier.

Elastic Beanstalk

Elastic Beanstalk offers a lot of bang for your buck. Deployment, auto scaling, load balancing and database management are just some of the features Elastic Beanstalk offers out-of-the-box.

It’s hard to find a software platform that isn’t supported by Elastic Beanstalk, it currently offers support for Ruby, PHP, Python, Java, .NET, Node.JS and Go. But it doesn’t stop there, for every platform there is a multitude of platform versions and servers to run your application. Just for the Ruby environment you can choose between several versions of Ruby in combination with Passenger or Puma.

But it doesn’t stop there! Elastic Beanstalk has one more trick up it’s sleeve: The ability to run your application inside a Docker container.

Why Docker?

There are a lot of advantages to using Docker for running your application. A very incomplete list:

  • Less overhead on the environment running your application.
  • Test and run your application in the same environment.
  • Easy to debug. You can export a faulty container, download it and start it locally.
  • Simplify configuration, less need for customising the host environment using .ebextensions.
  • Faster deployments and auto scaling.

We had been experimenting with using Docker to run our stack for a while. Our first try was using Deis to run our staging environment. We saw a lot of potential and spent quite a lot of time on getting our stack to work but, in the end we simply didn’t feel comfortable with the level of stability Deis was offering.

This made us little weary of trying it again on Elastic Beanstalk but, after some initial experimentation we were sold!

If you are interested in knowing more about our experiences using Deis please let us know in the comments. If we receive enough interest we might devote an article to the subject.

Our solution

There’s more than one way to go about deploying an application on an Elastic Beanstalk Docker environment. First of all, you have the choice between running a single or multi-docker container setup. For our purpose the Single Docker setup works just fine.

First, we are going to need a base docker image that we can use for our deployments:

FROM springest/ruby:2.1.5

RUN apt-get -y -q update && apt-get -y -q install build-essential python-dev python-pip && apt-get clean
RUN pip install awscli awsebcli

RUN echo 'gem: --no-ri --no-rdoc' > ~/.gemrc

# Rubygems and bundler
RUN gem update --system --no-ri --no-rdoc
RUN gem install bundler --no-ri --no-rdoc

RUN git config --global user.email "developers@awesome-app.com"
RUN git config --global user.name "AwesomeApp"

Build and push this to a Docker repository like Docker Hub or Quay.io:

docker build -t awesome_app/base .
docker tag awesome_app/base:latest awesome_app/base:0.1
docker push awesome_app/base

You should of course replace awesome_app with the name of your own organisation on the Docker repository.

Next we will create the Dockerrun.aws.json file. This file holds the configuration needed for Elastic Beanstalk to deploy your Docker container:

{
  "AWSEBDockerrunVersion": 1,
  "Ports": [
    {
      "ContainerPort": "3000"
    }
  ],
  "Volumes": [
    {
      "HostDirectory": "/var/app/current/webapp/tmp",
      "ContainerDirectory": "/webapp/tmp"
    }
  ],
  "Logging": "/webapp/log"
}

The file includes the following sections:

  • AWSEBDockerrunVersion - Specifies the version number. 1 for single and 2 for multi.
  • Ports - Which ports to expose on the Docker container.
  • Volumes - Which volumes should be mounted from host to the Docker container.
  • Logging - Where to mount the host log directory to the docker container. The host log directory can be found here: /var/log/eb-docker/containers/eb-current-app.

If you are already familiar with the structure of the Dockerrun.aws.json you might notice we left out the Image section. The above Dockerfile will be used as a base for a Docker image that will be built during deployment by Elastic Beanstalk. For this we need to create a different Dockerfile which will be sent along with all other application files to Elastic Beanstalk during deployment.

Create the following Dockerfile and place it in config/docker:

FROM awesome_app/base:0.1

ADD webapp /webapp/

WORKDIR /webapp

CMD bundle exec puma -C config/puma.rb

EXPOSE 3000

In this example we start the application by running puma but you could just as easily start any other application server.

For the final step we will need to pull it all together. We will be deploying from inside the awesome-app/base:0.1 docker container. For this we will create a simple script called deploy inside the script directory:

#!/bin/bash

function check_environment_status {
  environment=$1

  echo "Checking if $environment is not updating..."

  STATUS=`eb status $environment | grep Status | sed 's/^[ ]*Status: \(.*\)$/\1/'`

  if [ "$STATUS" = "Updating" ]; then
    echo "Environment $environment is updating. Aborting deploy."
    exit 1
  fi
}

function deploy_to_environment {
  environment=$1

  echo "Deploying to $environment..."

  git add --all .

  git commit -q -m "Deploying to ${environment}"

  eb deploy --nohang --quiet
}

BASE_DIR=$PWD
DEPLOY_DIR="$BASE_DIR/deploy"

EC2_REGION="eu-central-1"
ENVIRONMENT_NAME="awesome-app"

rm -rf $DEPLOY_DIR

mkdir -p $DEPLOY_DIR/{webapp/public,.elasticbeanstalk}

cd $DEPLOY_DIR

cat > $DEPLOY_DIR/.elasticbeanstalk/config.yml <<- EBC
branch-defaults:
  master:
    environment: ${ENVIRONMENT_NAME}
global:
  application_name: awesome-app
  default_ec2_keyname: awesomeappkey
  default_platform: Docker 1.5.0
  default_region: ${EC2_REGION}
  profile: null
  sc: git
EBC

mkdir -p /root/.aws

cat > /root/.aws/config <<- ACF
[default]
output = json
region = eu-central-1
aws_access_key_id = $AWS_KEY
aws_secret_access_key = $AWS_SECRET_KEY
ACF

git init .

check_environment_status ENVIRONMENT_NAME

echo "Copying Elastic Beanstalk files..."
cp -r $BASE_DIR/.ebextensions $DEPLOY_DIR
cp -f $BASE_DIR/Dockerrun.aws.json $DEPLOY_DIR
cp -f $BASE_DIR/config/docker/Dockerfile $DEPLOY_DIR

echo "Copying application files..."
cp $BASE_DIR/config.ru $DEPLOY_DIR/webapp/
cp $BASE_DIR/Gemfile $DEPLOY_DIR/webapp/
cp $BASE_DIR/Gemfile.lock $DEPLOY_DIR/webapp/
cp $BASE_DIR/Rakefile $DEPLOY_DIR/webapp/
cp -r $BASE_DIR/.bundle $DEPLOY_DIR/webapp
cp -r $BASE_DIR/app $DEPLOY_DIR/webapp
cp -r $BASE_DIR/config $DEPLOY_DIR/webapp
cp -r $BASE_DIR/db $DEPLOY_DIR/webapp
cp -r $BASE_DIR/lib $DEPLOY_DIR/webapp
cp -r $BASE_DIR/public/assets $DEPLOY_DIR/webapp/public
cp -r $BASE_DIR/public/images $DEPLOY_DIR/webapp/public
cp -r $BASE_DIR/script $DEPLOY_DIR/webapp

# Run bundle install here so we don't have to on every application server
cd $DEPLOY_DIR/webapp
bundle install --deployment -j4 --retry=3
cd $DEPLOY_DIR

# Remove any .git directory from gems that are downloaded from a git repository
find $DEPLOY_DIR/webapp/vendor/bundle -name ".git" -type d | xargs rm -rf

deploy_to_environment ENVIRONMENT_NAME

rm -rf $DEPLOY_DIR

In the example the EC2 region has been hardcoded to eu-central-1 but this can be changed to the region of your liking.

The script creates two files needed by the Elastic Beanstalk CLI to deploy your application:

  • .elasticbeanstalk/config.yml - This file specifies the target environment for deployment
  • /root/.aws/config - This file specifies the AWS credentials file needed for authentication

For the script to work it will need two environment variables:

  • $AWS_KEY
  • $AWS_SECRET_KEY

These can be generated from the IAM console in your AWS console.

Don’t forget to make the script executable:

chmod +x script/deploy

Finally, let’s run the script. First we will need to start a new docker container based on the awesome-app/base:0.1 image:

docker run --rm -it -v $PWD:/webapp awesome_app/base:0.1 /bin/bash

If everything went as planned you should be logged into the docker container as root.

To run the script we first have to cd to the /webapp directory so we can run the deploy script:

cd /webapp
AWS_KEY=FOO AWS_SECRET_KEY=BAR script/deploy

When the script is finished your application is pushed to Elastic Beanstalk for propagation to your EC2 instances, shaving a lot of time on spinning up new EC2 instances during auto scaling.

What’s next

Even though running the deployment from your local machine works perfectly fine, it would be a lot more efficient to let your CI environment take care of this. At Springest we use Wercker and their Ewok infrastructure to run our specs and deploy script.

This article won’t go into detail how Wercker can be used to make the process even more efficient. If you are interested in the details please leave a comment. If we receive enough interest we might devote an article about it in the future.