In the last article I described how to build a simple cookbook for deploying Node.JS applications using Chef. Check it out if you want to recapitulate the steps as we will re-use them here.

In this post I want to describe how to group your Chef code using Custom Resources, which were introduced with Chef 12.5.

Custom Resources

Resources are the most essential part of the Chef DSL like git, package or template. Once upon a time ... to create your own resources you had to use HWRP (heavy weight resource providers). Then LWRP (light weight resource providers) were introduced to make your life easier. Now there's this new kid on the block called Custom Resources and again it should make things even more simple. I don't want to go into details, but what I noticed first is the ability to run recipes from within your Custom Resource is now as simple as include_recipe.

So why do we even need to define our own resources?

Coming back to our Cookbook, we defined five steps that are necessary for the deployment:

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

First of all, not all of these steps are necessary for each deployment. Secondly Chef Resources are easier to handle, than include_recipe syntax and thirdly they are easier to test, especially for users of your cookbook, who don't need to stub things that come from within you cookbook logic.

Ok, now we decided to build a Custom Resource for our deployment, how does it look like and how do we split it? As I mentioned before we might not want to run all steps for each deployment. This decision is more subjective and for our deployment I decided to not try to install git (1) or Node.js (2) on each run, whereas code updates (3), npm dependencies (4) and the app run environment should be checked on each run. This doesn't mean you cannot update Node.js later on, but I will come to this later.

Now we know how to split up functionality, we will create two Custom Resources, each of them must be one file located in the cookbook's directory within a resources folder. The fist one will be named setup.rb and the other one deploy.rb. The filename plus it's cookbook will give the resource it's name, for simplicity we will assume this cookbook is called node-app. So afterwards we will have the two resources node_app_setup and node_app_deploy.

Enough talking, let's write some code :)

# resources/setup.rb
property :nodejs_version, String, default: '5.10.1'
property :nodejs_checksum, String

default_action :run

action :run do
  # 1. install git
  include_recipe 'git'

  # 2. install nodejs
  node.default['nodejs']['install_method'] = 'binary'
  node.default['nodejs']['version'] = nodejs_version
  node.default['nodejs']['binary']['checksum']['linux_x64'] = nodejs_checksum
  include_recipe 'nodejs'
# resources/deploy.rb
property :ssh_key, String
property :dir, String, required: true
property :git_repository, String, required: true
property :git_revision, String, default: 'master'
property :service_name, String, required: true, name_property: true
property :run_cmd, String, default: '/usr/local/bin/npm start'
property :run_environment, Hash, default: {}

default_action :run

action :run do
  file '/root/.ssh/id_rsa' do
    mode '0400'
    content ssh_key

  git dir do
    repository git_repository
    revision git_revision
    action :sync

  execute "npm prune #{service_name}" do
    command 'npm prune'
    cwd dir

  execute "npm install #{service_name}" do
    command 'npm install'
    cwd dir

  template "/etc/init/#{service_name}.conf" do
    source 'upstart.conf.erb'
    cookbook 'node-app'
    mode '0600'
      name: service_name,
      chdir: dir,
      cmd: run_cmd,
      environment: run_environment
    notifies :stop, "service[#{service_name}]", :delayed
    notifies :start, "service[#{service_name}]", :delayed

  service service_name do
    provider Chef::Provider::Service::Upstart
    action :start
    subscibes :restart, "git[#{dir}]", :delayed
    subscibes :restart, "execute[npm prune #{service_name}]", :delayed
    subscibes :restart, "execute[npm install #{service_name}]", :delayed
# templates/default/app.conf.erb
description "Upstart Job for the <%= @name %> service"

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

chdir <%= @chdir %>

<% @environment.each do |key, val| %>
env <%= key %>=<%= val %>
<% end -%>


exec <%= @cmd %>

So what did we do here. Most of the code is copied from the last blog post, but what changed is, that our Chef code is part of an action block and strings we hard-coded before are now defined as properties. Check out the Chef docs for Custom Resources to learn more details. This means we can now call the resources from outside and configure them for our needs, like this:

# we need to add `depends 'node-app'` to our metadata.rb file

# let's setup everything for `myapp`
node_app_setup 'myapp' do
  nodejs_version '5.10.1'
  nodejs_checksum '...'

# let's deploy `myapp`
node_app_deploy 'myapp' do # service_name defaults to this name
  ssh_key '...'
  dir '/opt/myapp'
  git_repository '<repository>'
  git_revision 'master'
  run_cmd 'npm start'
    'NODE_ENV' => 'production',
    'PORT' => 8080

Doesn't it look amazing?

As an user, you don't need to care anymore what's happening behind the scenes, but as the maintainer you can easily change things in the background without anyone telling! Also both functionalities can still be triggered on each deployment, but it's simplier now to just run the node_app_deploy resource on each run and node_app_setup only on one intial run.

Chefspec Matcher

As a side note I want to mention, that with the created resources it's now simpler for a user of your cookbook to run tests using chefspec, as the normal spec run does not step into resources. To make the life of your user's simpler, every resource should come with a chefspec matcher! It's not more than a matchers.rb file inside your cookbook within the libraries folder, that looks like this:

# libraries/matchers.rb
if defined?(ChefSpec)
  def run_node_app_setup(name), :run, name)

  def run_node_app_deploy(name), :run, name)

In any chefspec test it can now be used like:

it 'should setup the server for myapp' do
  expect(chef_run).to run_node_app_setup('myapp').with(
    nodejs_version: '5.10.1',
    nodejs_checksum: '...'


So let's review what we did: We encapsulated the logic for our deployment into two Custom Resources, node_app_setup and node_app_deploy to give the user the possibility to use our deployment cookbook for multiple different configurations and provided a matchers.rb file to simplify external testability.

In the next article we will see how to use our new resources with AWS OpsWorks and the data OpsWorks injects.