< Return to Blog

Simple Scalable Infrastructure with DigitalOcean, Terraform, and Docker

While my primary system is a Mac, I like to keep my development workflows as linux dependent as possible. Therefore, rather than relying on tools such as boot2docker, I setup a custom image of Ubuntu 14.04 with various tools installed along with Docker 1.3 using Packer

My build template can be found on Github. My workflow from this point onwards is to simply start my customised version of Ubuntu via vagrant up and ssh [email protected].

Docker Build

Last weekend, I focused on creating a docker build for Rails, Nginx, and Unicorn with a MySQL data store. My aim at the time was to build a proof of concept, so I broke this down into several sessions of hacking.

The first successful iteration comprised of a couple scripts, which contained hard-links to locations on my system. Running my scripts would launch the containers within my development linux host. I was also able to verify this by cloning repos to my laptop and running the build there.

On Saturday, the 1st of November 2014, I set to decouple the build, so that it could be easily deployed on a remote instance. Ultimately, I was able to reduce the deployment aspect to two separate scripts, (i) the app deploy and (ii) load-balancer deploy,

docker-rails-deploy -> % REBUILD=true DEPLOY=true ./deploy.sh
# REBUILD: rebuild base images
# DEPLOY: run `git pull` within the cloned app repo.

docker-rails-deploy -> % REBUILD=true ./deploy-load-balancer.sh
# REBUILD: rebuild base images

Much of my efforts went into making these scripts as simple to use — they will automatically stop and remove containers, before restarting them. They will also cleanup stale images from 'rebuilds'.

This build is available via Github.

Orchestrated Provisioning with Terraform

However, I needed to address the orchestration aspect and decided to get started with Terraform. Having found 'How To Use Terraform with DigitalOcean' getting up to speed was a breeze, leading to a separate repo 'terraform-orchestrate'. Each 'plan' consists of,

# provider.tf
variable "do_token" {}
variable "pub_key" {}
variable "pvt_key" {}
variable "ssh_fingerprint" {}

provider "digitalocean" {
  token = "${var.do_token}"
}

The variable above are set as env variables and are set when running terraform.

# terraform.tf
resource "digitalocean_droplet" "inertialbox-balancer-1" {
  image = "ubuntu-14-04-x64"
  name = "inertialbox-balancer-1"
  region = "sgp1"
  size = "2gb"
  private_networking = true
  ssh_keys = [
    "${var.ssh_fingerprint}"
  ]

  connection {
    user = "root"
    type = "ssh"
    key_file = "${var.pvt_key}"
    timeout = "2m"
  }

  provisioner "remote-exec" {
    inline = [
      # ...host setup.
    ]
  }
}

Typically, one would combine such a setup with the likes of Ansible to aid in maintaining 'host' management. This becomes are more apparent concern when you're working with a large team and want to say centrally manage SSH access. It's far easier to have a playbook setup various public keys, rather than doing this by hand.

However, I want to keep things simple by trying to keep the abstractions to a minimum. It's a good idea to check the terraform plan before applying it, remembering that it grabs the DigitalOcean DO_PAT API key and SSH_FINGERPRINT from the environment,

terraform plan \
  -var "do_token=${DO_PAT}" \
  -var "pub_key=$HOME/.ssh/id_rsa.pub" \
  -var "pvt_key=$HOME/.ssh/id_rsa" \
  -var "ssh_fingerprint=${SSH_FINGERPRINT}"

Once applied, terraform will spin up a new droplet as specified and perform everything provided via remote-exec. Once this completes successfully, you will be left with a *.tfstate file, which is a representation of the state of infrastructure. It will be used the next time you make changes as a means of context.

{
    "version": 1,
    "serial": 1,
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {
                "digitalocean_droplet.inertialbox-balancer-1": {
                    "type": "digitalocean_droplet",
                    "primary": {
                        "id": "30***",
                        "attributes": {
                            "id": "30***",
                            "image": "ubuntu-14-04-x64",
                            "ipv4_address": "128.*",
                            "ipv4_address_private": "10.*",
                            "locked": "false",
                            "name": "inertialbox-balancer-1",
                            "private_networking": "true",
                            "region": "sgp1",
                            "size": "",
                            "ssh_keys.#": "1",
                            "ssh_keys.0": "***",
                            "status": "active"
                        }
                    }
                }
            }
        }
    ]
}

It took less than 30 mins to have multiple folders setup — one for each 'droplet' — allowing me to spin these up,

Live: Provisioning and Deployment

Here's a quick video of running two of these orchestration plans — on the left the load-balancer is being deployed and on the right the first app instance

Once they'd deployed, I needed to place some private SSH keys and 'trigger' the docker deploy script of my choice. There's a certain level of manual intervention at this point, but it does offer a level of flexibility. I can rebuild the load-balancer at any time, updating ip addresses of the failover instances as I wish.

Having run the deploy script, here's the status of the various containers on the 'app-1' instance

CONTAINER ID        IMAGE                                         COMMAND                CREATED             STATUS              PORTS                    NAMES
ae0c53e3f38f        inertialbox/inertialbox-app-failover:latest   "/bin/sh -c 'foreman   42 hours ago        Up 42 hours         0.0.0.0:8081->80/tcp     app-failover
2827c7ec7dc3        inertialbox/inertialbox-app:latest            "/bin/sh -c 'foreman   42 hours ago        Up 42 hours         0.0.0.0:8080->80/tcp     app
75244b185887        mysql:5.7                                     "/entrypoint.sh mysq   2 days ago          Up 2 days           0.0.0.0:3306->3306/tcp   mysql

With the load balancer deploy also having completed, here's the money shot,

inertialbox.com has now been deployed using this stack. The videos you saw above, were from the live deploy. Currently, it comprises of 3-droplets — two in Singapore and a failover app instance at DigitalOcean's NYC3 centre.

Singapore has both the main load-balancer and the primary app instance, saving here is that the load-balancer proxies to the app instance over the private network (bandwidth is free!) - I'll only incur charges when the failover is 'active'.

In terms of cost, 2GB instances at $0.03/hr * 3 = $0.09/hr * 24 = $2.16/day or ~$60/- per month.

Conclusion

So, let's review the benefits of this setup,

Pros

  • Identical stack can be deployed to Amazon AWS or Google Cloud
  • Provisioned instances can be placed in any of the regions, depending on the chosen provisioner.
  • Deploy build can be custom tailored to the application's and developing teams needs, i.e. include Redis and switch to PostgresSQL.

Cons

  • As per my demo, host management via Ansible/Chef has not been demonstrated; however, this could be added quite easily. The workflow would be, (i) provision via terraform, (ii) run Ansible playbook, (iii) run Docker deploy script.
  • Each provisioned node runs in an isolated fashion — this is not a cluster and were unable to receive health checks etc.
What's next?

I will be looking into CoreOS to try and aid into improving this platform and hope to bring you another post shortly.

Until then, I suggest you take the plunge and give DigitalOcean a try — let me know how it went!

Update — Saturday, 15th November 2014

Having added a portfolio showcase section to the Inertialbox site, here's how simple my deploy process was. Simply SSH into the app-1 instance on DigitalOcean,

root@inertialbox-app-1:/opt/docker-rails-deploy# REBUILD=true DEPLOY=true ./deploy.sh
remote: Counting objects: 62, done.
remote: Compressing objects: 100% (57/57), done.
remote: Total 62 (delta 27), reused 0 (delta 0)
Unpacking objects: 100% (62/62), done.
From bitbucket.org:bsodmike/inertialbox.com
   15c79a1..662c4b0  master     -> origin/master
Updating 15c79a1..662c4b0
Fast-forward
 app/assets/images/covers/creative_ui_red_bg.jpg       | Bin 0 -> 648806 bytes
 app/assets/images/portfolio/andythornton.jpg          | Bin 0 -> 164142 bytes
 app/assets/images/portfolio/autoglym-professional.jpg | Bin 0 -> 84749 bytes
 app/assets/images/portfolio/autoglym.jpg              | Bin 0 -> 141766 bytes
 app/assets/images/portfolio/poshpaws.jpg              | Bin 0 -> 86999 bytes
 app/assets/stylesheets/theme/inertialbox/base.scss    |  68 ++++++++++++++++++++++++++++++++++-
 app/views/public/index.html.erb                       |  81 +++++++++++++++++++++++++++++++++++++-----
 7 files changed, 140 insertions(+), 9 deletions(-)
 create mode 100644 app/assets/images/covers/creative_ui_red_bg.jpg
 create mode 100644 app/assets/images/portfolio/andythornton.jpg
 create mode 100644 app/assets/images/portfolio/autoglym-professional.jpg
 create mode 100644 app/assets/images/portfolio/autoglym.jpg
 create mode 100644 app/assets/images/portfolio/poshpaws.jpg
Already up-to-date.

===> Fetched latest app changes from git repo '[email protected]:bsodmike/inertialbox.com.git'


===> Commencing app rebuild...

Sending build context to Docker daemon 3.665 MB
Sending build context to Docker daemon
Step 0 : FROM inertialbox/rails-nginx-unicorn
# Executing 7 build triggers
Trigger 0, ADD Gemfile /home/app/Gemfile
Step 0 : ADD Gemfile /home/app/Gemfile
 ---> Using cache
Trigger 1, ADD Gemfile.lock /home/app/Gemfile.lock
Step 0 : ADD Gemfile.lock /home/app/Gemfile.lock
 ---> Using cache
Trigger 2, RUN bundle install --without development test
Step 0 : RUN bundle install --without development test
 ---> Using cache
Trigger 3, ADD . /home/app
Step 0 : ADD . /home/app
Trigger 4, RUN mkdir -p /home/app/public/assets
Step 0 : RUN mkdir -p /home/app/public/assets
 ---> Running in af14fa331cdc
Trigger 5, RUN bundle exec rake assets:precompile
Step 0 : RUN bundle exec rake assets:precompile
 ---> Running in b372ce75c3ab
 I, [2014-11-14T20:45:58.005322 #7]  INFO -- : Writing /home/app/public/assets/covers/creative_ui_red_bg-
 eaecfc6d818562481f3bc1ff1e49d45e.jpg
 I, [2014-11-14T20:45:58.008760 #7]  INFO -- : Writing /home/app/public/assets/featured/team-25236e42f8d1
 9262da42fdc3ad4d83f3.jpg
 I, [2014-11-14T20:45:58.010732 #7]  INFO -- : Writing /home/app/public/assets/icons/attendance-85fbc6a0c
 73ace66489a80fa13651ec8.svg
 I, [2014-11-14T20:45:58.012551 #7]  INFO -- : Writing /home/app/public/assets/icons/close-c872e7a0fb259a
 da5f40acca6fc6cf55.svg
 I, [2014-11-14T20:45:58.014375 #7]  INFO -- : Writing /home/app/public/assets/icons/menu-970567837171e83
 018aa13fd7263a978.svg
 I, [2014-11-14T20:45:58.016568 #7]  INFO -- : Writing /home/app/public/assets/portfolio/andythornton-60f
 c200f7d9c87ccc85df338a98be54a.jpg
 I, [2014-11-14T20:45:58.018745 #7]  INFO -- : Writing /home/app/public/assets/portfolio/autoglym-profess
 ional-a701939238476e4ddeba66972708d331.jpg
 I, [2014-11-14T20:45:58.021148 #7]  INFO -- : Writing /home/app/public/assets/portfolio/autoglym-7814f13
 d658d0ff2073eb986fd33cb42.jpg
 I, [2014-11-14T20:45:58.023372 #7]  INFO -- : Writing /home/app/public/assets/portfolio/poshpaws-cf3a5ea
 d0667c022f094a2bf8d049bc2.jpg
 I, [2014-11-14T20:46:02.768546 #7]  INFO -- : Writing /home/app/public/assets/application-7adc94a23f3a1c
 9c75fa631c94c426be.js
 I, [2014-11-14T20:46:08.198599 #7]  INFO -- : Writing /home/app/public/assets/application-8142b3acee80b1
 bb7b6620db1ec69d0f.css
 Trigger 6, RUN chown -R www-data:www-data /home/app/public/assets
 Step 0 : RUN chown -R www-data:www-data /home/app/public/assets
  ---> Running in adf34b9b3f93
  ---> 1b395eb940db
 Removing intermediate container 765369a53c75
 Removing intermediate container af14fa331cdc
 Removing intermediate container b372ce75c3ab
 Removing intermediate container adf34b9b3f93
 Step 1 : MAINTAINER Michael de Silva <[email protected]>
  ---> Running in 301005abd3fc
  ---> 817ec48833ff
 Removing intermediate container 301005abd3fc
 Step 2 : EXPOSE 80
  ---> Running in b316c50dbec4
  ---> 78e233ca0df7
 Removing intermediate container b316c50dbec4
 Successfully built 78e233ca0df7
 Sending build context to Docker daemon  1.33 MB
 Sending build context to Docker daemon
 Step 0 : FROM inertialbox/rails-nginx-unicorn-failover
 # Executing 7 build triggers
 Trigger 0, ADD Gemfile /home/app/Gemfile
 Step 0 : ADD Gemfile /home/app/Gemfile
  ---> Using cache
 Trigger 1, ADD Gemfile.lock /home/app/Gemfile.lock
 Step 0 : ADD Gemfile.lock /home/app/Gemfile.lock
  ---> Using cache
 Trigger 2, RUN bundle install --without development test
 Step 0 : RUN bundle install --without development test
  ---> Using cache
 Trigger 3, ADD . /home/app
 Step 0 : ADD . /home/app
 Trigger 4, RUN mkdir -p /home/app/public/assets
 Step 0 : RUN mkdir -p /home/app/public/assets
  ---> Running in 4a077701e79d
 Trigger 5, RUN bundle exec rake assets:precompile
 Step 0 : RUN bundle exec rake assets:precompile
  ---> Running in 935b0e069c4a
 I, [2014-11-14T20:46:13.284143 #8]  INFO -- : Writing /home/app/public/assets/featured/team-25236e42f8d1
 9262da42fdc3ad4d83f3.jpg
 I, [2014-11-14T20:46:13.286598 #8]  INFO -- : Writing /home/app/public/assets/icons/attendance-85fbc6a0c
 73ace66489a80fa13651ec8.svg
 I, [2014-11-14T20:46:13.288566 #8]  INFO -- : Writing /home/app/public/assets/icons/close-c872e7a0fb259a
 da5f40acca6fc6cf55.svg
 I, [2014-11-14T20:46:13.290420 #8]  INFO -- : Writing /home/app/public/assets/icons/menu-970567837171e83
 018aa13fd7263a978.svg
 I, [2014-11-14T20:46:18.790460 #8]  INFO -- : Writing /home/app/public/assets/application-7adc94a23f3a1c
 9c75fa631c94c426be.js
 I, [2014-11-14T20:46:24.291391 #8]  INFO -- : Writing /home/app/public/assets/application-0a7b0f5a3ef9eb
 7a3f77238d8eec4989.css
 Trigger 6, RUN chown -R www-data:www-data /home/app/public/assets
 Step 0 : RUN chown -R www-data:www-data /home/app/public/assets
  ---> Running in 725e420a2db0
  ---> 772d5127cf1f
 Removing intermediate container 1e3eef603bcf
 Removing intermediate container 4a077701e79d
 Removing intermediate container 935b0e069c4a
 Removing intermediate container 725e420a2db0
 Step 1 : MAINTAINER Michael de Silva <[email protected]>
  ---> Running in 8b2e40d4b62d
  ---> 0cba1afceeac
 Removing intermediate container 8b2e40d4b62d
 Step 2 : EXPOSE 80
  ---> Running in ef4fc325b690
  ---> a390144b05f8
 Removing intermediate container ef4fc325b690
 Successfully built a390144b05f8

 ===> Completed app rebuild.


 ===> Removing stale images.

 Error response from daemon: Conflict, cannot delete c96d068b1974 because the running container 49e139669
 d43 is using it, stop it and use -f to force
 Error response from daemon: Conflict, cannot delete 290d4400eaf4 because the running container 87eec696d
 69a is using it, stop it and use -f to force
 2014/11/14 15:46:25 Error: failed to remove one or more images

 ===> Stopping and removing app container.


 ===> Stopping and removing app-failover container.


 ===> Linking and running app instance...

 ef236c590d69bd999e1360a2802e630996dc90179270e718a17843b94ef19e21

 ===> Linking and running app failover instance...

 0ff13a1fa670e836dc3fd0d7b8563a4252f13701e31455950323e577887725d2

 ===> Done.

All I had to do was run a single command REBUILD=true DEPLOY=true ./deploy.sh inside REBUILD=true DEPLOY=true ./deploy.sh.

Rinse and repeat for app-2. Once I script this as well, it'll truly be a single-click deploy.