Deploying Docker Containers to AWS using CloudBees CodeShip, CodeDeploy and Chef

Written by: Amit Saha

In this article, we will see how we can deploy a web application as a Docker container to Amazon AWS EC2 instances using CloudBees CodeShip's integration with AWS CodeDeploy. We will integrate Chef configuration management with AWS CodeDeploy to perform the deployment.

To follow along with this article, I recommend cloning the Git repository. In addition, creating a fork of the repository will allow you to deploy the sample web application on your own AWS infrastructure via your own continuous integration (CI) setup. That, of course, assumes you have:

  • An AWS account setup

  • A CloudBees CodeShip Pro account (Free tier is sufficient)

  • Terraform 0.11.x installed locally

Let's get started.

Sample web application

Our web application is a simple HTTP server written in Golang, listens on port 8080 and returns a simple text response upon requesting the index page. If you have Docker installed, you can build the Docker image and run the container binding the host port 80 to the container port 8080.

In one terminal:

$ <repository root>
$ cd webapp
$ docker build -t amitsaha/webapp-demo:golang .
$ docker run -d -P 80:8080 amitsaha/webapp-demo:golang

In a different terminal:

$ curl
Hello, world! I am a web application

Our goal for this article is to deploy this Docker container to AWS EC2 instance. We will push the built image to Docker Hub as part of our CI setup.

CodeDeploy configuration

To be able to deploy the web application container using AWS CodeDeploy, we specify a appspec.yml - stored in the webapp/deployment sub-directory and is as follows:

version: 0.0
os: linux
    - location: ./cd_eventhandler.bash
    - location: ./cd_eventhandler.bash
    - location: ./cd_eventhandler.bash
    - location: ./cd_eventhandler.bash
[Hooks]( in AppSpec file specify various actions to be taken during stages of the deployment lifecycle. The lifecycle stages and their order vary based on the deployment platform that we select which in our case is EC2. We will be using in-place deployment without a load balancer. Details on the hooks and various lifecycle stages can be found in [AWS documentation](

The `cd_eventhandler.bash` stored in the same `deployment` sub-directory is as follows:


set -eu

We need to do the below steps once per deployment and not every lifecycle stage

Hence, we use the first lifecycle event, APPLICATION_STOP to do this.

if ; then
pushd /etc
aws s3 cp s3://aws-codedeploy-chef-demo/ /etc/
rm -rf chef
unzip -o
pushd chef chef-solo -c ./solo.rb --override-runlist "recipe"

For the first deployment to an EC2 instance, the ApplicationStop lifecycle

hook is not executed, hence, we download the chef cookbooks if the /etc/chef

directory does not exist and execute the base recipe

if ; then
aws s3 cp s3://aws-codedeploy-chef-demo/ /etc/
pushd /etc
unzip -o
pushd chef
-c ./solo.rb --override-runlist "recipe"
pushd /etc/chef
chef-solo -c ./solo.rb --override-runlist "recipe"

For the lifecycle event, `ApplicationStop` which is the first event that will be executed during a deployment, we download a zip file containing the `Chef` cookbooks, unzip it and execute the `base` recipe in the `webapp` cookbook via `chef-solo`. For all other lifecycle stages, we execute the recipe named `CDHook_<lifecycle event>` from the same cookbook. As you can see in a comment above, for the first deployment to an EC2 instance, the `ApplicationStop` lifecycle hook is never executed so we check that for any lifecycle stage. If we don't have the `/etc/chef` directory, we download the zip file, extract it and run the `base` recipe from the `webapp` cookbook.

Chef cookbooks

The chef cookbook for deploying our sample web application can be found in the `chef/cookbooks/` sub-directory. The `webapp/recipes` sub-directory contain the recipes for our application:
  • base.rb

  • CDHook_ApplicationStop.rb

  • CDHook_AfterInstall.rb

  • CDHook_ApplicationStart.rb

  • CDHook_ValidateService.rb

The `base` recipe installs the Docker engine (if not already installed), with the subsequent steps corresponding to an AWS CodeDeploy lifecycle hook we care about. The `CDHook_ApplicationStop` recipe currently stops the container named `service` and then removes it. In a more practical scenario, before the container is stopped, we would take the application instance out of the application pool so that new requests do not get sent to this instance and existing requests are processed.

The `CDHook_AfterInstall` recipe which runs after `ApplicationStop` pulls the latest Docker image for the web application and creates a new `service` container from this image.

The `CDHook_ApplicationStart` recipe as it stands now just has a sleep for an artbitrary number of seconds to give our application to be ready. We then validate whether our application is ready by making a HTTP request to port 80 in the `CDHook_ValidateService` recipe.

Setting up an AWS infrastructure

We will need to set up a few things in our AWS infrastructure before we can run a trial to deploy our application:
  • VPC and subnets

  • CodeDeployment application and associated resources

  • S3 bucket for storing deployment artifacts

  • AutoScaling group, launch configuration and security groups

  • IAM profiles, roles and policies

The `infra` sub-directory has the [Terraform]( code for creating the entire infrastructure needed for this article. You will need AWS admin level privileges to be able to create all the resources successfully. Once you have Terraform installed and your AWS credentials have been supplied via one of the [supported means](, run `terraform apply`, like so:

$ <repo root>
$ cd infra
$ terraform apply --var-file=input.tfvars</repo>

There are two variables defined in ``:

variable "ssh_pub_key_path" {}
variable "ssh_whitelist_cidrs" {
type = "list" default = []

The `ssh_pub_key_path`, must be set to the path of your SSH public key that you may want to SSH into your EC2 instances with. By default, it is set to `~/.ssh/`. You can change it in `input.tfvars` or provide it by one of the other [supported means]( The `ssh_whitelist_cidrs` is a list of IPv4 addresses that will be added to the security group to allow SSH connections from. Since we can change this at runtime without needing to recreate an EC2 instance, this is left by default to be an empty list, and we can add it if we need to SSH into an instance.

Once the `terraform apply` has completed, it will output the public IPv4 address of the instance that is created. We can always retrieve it by running `terraform apply` again at a later time.

Now that our infrastructure is set up, let's create our continuous integration setup on CloudBees CodeShip Pro.

Setting up continuous integration in CloudBees CodeShip Pro

The key files for CloudBees CodeShip Pro are `codeship-services.yml` and `codeship-steps.yml`. The `codeship-services.yml` file defines the docker containers that we will use in the `codeship-steps.yml` file, which is where all the different steps are defined.

We will go through the different sections of the `codeship-services.yml` next. First, we configure our application container building:

image: amitsaha/webapp-demo:golang
context: webapp
dockerfile: Dockerfile

The context specifies the `webapp` directory which is where our web application's source code and the `Dockerfile` is.

Next, we define how we want to build the container for building our `chef` artifact:

image: amitsaha/chef-builder context: .
dockerfile: Dockerfile.chef
encrypted_env_file: aws-deployment.env.encrypted
- ./:/deploy

The `Dockerfile.chef` builds a docker image based on Ubuntu 18.04 with [chef workstation]( installed. We specify our encrypted AWS access credentials file via the `encrypted_env_file` specification. To create this file, we follow the instructions described [here]( using the [jet cli](

The access keys above correspond to the user `webapp` which we specifically create for this application. We can generate a access key using:

$ aws iam create-access-key --user-name webapp

The IAM policy that is attached with this user allows it to create a deployment on the code deployment group and perform all operations on the S3 bucket we create for storing the deployment artifacts. The IAM user and policy creation is configured in `infra/` and looks as follows:

"Version": "2012-10-17",

We provide the AWS credentials so that we can publish the `chef` cookbooks to the S3 bucket setup for the deployment.

The next container is the `curl` container which we use for running a sanity check on the built web application container:

curl: image: pstauffer/curl:latest depends\_on: \["myapp"\]

Finally, we configure the `codeship/aws-deployment` container, which we use to create a CodeDeploy deployment:

image: codeship/aws-deployment
encrypted_env_file: aws-deployment.env.encrypted
- ./:/deploy
- AWS_DEFAULT_REGION=ap-southeast-2

Once again, we specify the `aws-deployment.env.encrypted` file for this container since we will be performing a deployment using this Docker image.

The `codeship-steps.yml` file defines the various build steps. First, we run the basic sanity check on our web application container:
  • service: myapp
    name: Push docker image for application
    type: push
    image_name: amitsaha/webapp-demo:golang
    image_tag: golang
    encrypted_dockercfg_path: dockercfg.encrypted

In the next two steps, we push our `chef_builder` Docker image which we then use to build the Chef artifact:
  • service: chef_builder
    name: Push chef builder
    type: push
    image_name: amitsaha/chef-builder
    encrypted_dockercfg_path: dockercfg.encrypted

  • service: chef_builder
    name: Build and deploy chef artifact
    command: bash /deploy/

You should replace the above docker image names and the Docker Hub credentials with your own to be able to perform these steps successfully.

In the final step, we use [CloudBees CodeShip's]( manual approval feature to wait for an approval before deploying our web application:
  • type: manual
    tag: controller

    • service: awsdeployment
      name: Deploy
      command: codeship_aws codedeploy_deploy /deploy/webapp/deployment webapp webapp-test aws-codedeploy-chef-demo

![aws codeship sample](

 _Example deployment blocked on user approval_

The parameters to the `codedeploy_deploy` command are:

*   `/deploy/webapp/deployment`: This is the deployment directory for our web application. A `zip` file will be created from this directory and uploaded to the S3 bucket `aws-codedeploy-chef-demo`
*   `webapp-test`: The CodeDeploy deployment group
*   `aws-codedeploy-chef-demo`: S3 bucket used for storing the CodeDeploy artifacts.

Setting up your own project

We have now created the AWS infrastructure and set up continuous integration and deployment in CloudBees CodeShip Pro. If you have created a fork of the demo repository, you will need to update a few things to match with your own setup.

*   The Docker hub image name and credentials should be replaced to match with your account.
*   The Chef recipes must be updated to use the updated docker image.
*   The AWS credentials must be updated to match your account's credentials.
*   Create your own CloudBees CodeShip Pro project.

Once you have done the above, you should be all set and if you go the url `http://<IP>` which matches the IP address of your EC2 instance, you should see, `Hello, world! I am a web application`.


In this article, we saw how we can take advantage of CloudBees CodeShip Pro's AWS CodeDeploy integration to deploy a web application to AWS EC2 instances using AWS CodeDeploy. We saw how we can use `chef` cookbooks with AWS CodeDeploy allowing us to integrate configuration management into our deployment pipeline. In a practical scenario, the chef cookbooks and infrastructure code will be managed separately, of course.

The GitHub repository has all the resources to help you achieve the same for your own projects. 

Additional resources

*   [CodeShip Pro Integration for AWS CodeDeploy](
*   [AWS CodeDeploy](
*   [Chef solo](

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.