At blogfoster we love AWS, Node.js and the philosophy of microservices. As we built more and more microservice, we needed to come up with an automated deployment.

Our current development workflow is based on Docker, but we're quite experienced with Chef and AWS Opsworks, so we wanted to start with these tools, to quickly built a solid solution. Nevertheless, we will try to use a Docker-based solution for the future as we know Docker deployments are amazingly fast.

In this first article, we want to present a simple approach for deploying Node.js applications using Chef.

The application setup

Before going into details it's always about assumptions, and so does this article. The following deployment works well in cases you rarely need to scale and are searching for a simple deployment strategy on AWS.

For applications that need to scale more often, I would advise you to read about ami-backed deployments. Also in the context of microservices, for us it is ok if a service is unavailable for a few seconds.

Opsworks Node.js deployment

As AWS Opsworks started supporting Chef 12, they dropped the support for their built-in cookbooks (which imho, was the right decision, as the Chef deployment becomes more transparent now). Sure you can download their cookbooks and just use them, but looking at the code reveals that they're not dead-simple.

So modifying them to your needs might be challenging. Ignoring that, the setup is not using standard distro tools like upstart/systemd on Ubuntu and it's trying to do a Capistrano-like deployment (as it's using the internal Chef resource deploy).

Don't get me wrong, these cookbooks are battle-proofed in production for quite some time, and a lot of effort was put into them, so I will not argue against them, but for me they are too bloated.

I mean getting the full code (git repository) and downloading all node dependencies for every deployment for a medium sized project on a t2.micro instance could easily take more than getting a coffee playing a round of dart and getting the second coffee.

Not actually continuous deployment ...

Simple deployments with Chef

So how could a simple deployment look like? Let's collect some bullet points:

  1. install git
  2. install nodejs
  3. get the code through git/github
  4. install external node packages
  5. run the app

Should be simple, right? The devil is in the detail!

What does it mean to install something? Ideally, if the particular version is not there install it, otherwise skip this step. This helps for the first two steps, but how do you get the code from git?

First, we need to provide credentials, e.g. using a ssh key file, then sync/check-out the explicit branch. Next, and one of the most interesting parts: installing external node packages. To keep the deployment run as short as possible, we have to download all the dependencies on the first run, but for all following deployments we should just download those dependencies that were updated. So what does it mean for real?

Nothing more than: npm prune and npm install! The npm prune command removes any out-dated dependencies and npm install by it's nature installs packages, but also skips those that are already installed (ok, npm-shrinkwrap performs the fastest but without it, it's also fine).

And last but not least, how should we run the app? It should be failure safe when crashing! Since we're running our software on Ubuntu 14.04 we use the built-in tool upstart, which can handle all of that. Talking about running the app, we should remember to also restart our app if there are any changes to code, dependencies or run environment!

Let's recap the strategy:

  1. install git
    • install git if it's not there
    • skip it if git is already installed
  2. install nodejs
    • install specific nodejs version
    • skip it if it's already installed
  3. get the code through git/github
    • provide github access using a ssh key file
    • sync the latest code for given branch or tag
  4. install external node packages
    • download all dependencies if they're not there
    • only intall updated dependencies if there were installed already once, and remove out-dated once (npm prune && install)
  5. run the app
    • make us of upstart to start the node app
    • restart the app in case the code changes, any dependency were updated or changes to the run environment

Now we can translate this to chef using the git and nodejs community cookbooks.

  • (1) install git
include_recipe 'git'
  • (2) install nodejs
node.default['nodejs']['install_method'] = 'binary'
node.default['nodejs']['version'] = '5.10.0'
node.default['nodejs']['binary']['checksum']['linux_x64'] = '...'

include_recipe 'nodejs'
  • (3) get the code through git/github
file '/root/.ssh/id_rsa' do
  mode '0400'
  content '...'

git '/opt/app' do
  repository '<repository>'
  revision '<revision>'
  • (4) install external node packages
execute 'npm prune' do
  cwd '/opt/app'

execute 'npm install' do
  cwd '/opt/app'
  • (5) run the app
### /templates/default/app.conf.erb
description "Upstart Job for the App service"

start on (local-filesystems and net-device-up IFACE!=lo)
stop on shutdown

setuid root
chdir /opt/app

env NODE_ENV=production
env PORT=8080


exec npm start
template '/etc/init/app.conf' do
  notifies :stop, 'service[app]', :delayed
  notifies :start, 'service[app]', :delayed

service 'app' do
  provider Chef::Provider::Service::Upstart
  action :start
  subscibes :restart, 'git[/opt/app]', :delayed
  subscibes :restart, 'execute[npm prune]', :delayed
  subscibes :restart, 'execute[npm install]', :delayed

And that's already it!

Ok with that I wasn't honest, in real life you will probably create an app user with dropped privileges and do all the steps with that user (ssh key file, git, npm ...) and you might need to create a known_hosts entry for github initially, but then we're done :)

So let's review what we did: We deployed a Node.js app using Chef with a few simple steps, which are installing git, nodejs, the code, npm packages and lastly we also started the app using upstart.