Deploying Node.js Applications with AWS OpsWorks - part 2

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'
end  
# 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
  end

  git dir do
    repository git_repository
    revision git_revision
    action :sync
  end

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

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

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

  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
  end
end  
# 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 -%>

respawn

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 '...'
end

# 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'
  run_environment(
    'NODE_ENV' => 'production',
    'PORT' => 8080
  )
end  

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)
    ChefSpec::Matchers::ResourceMatcher.new(:node_app_setup, :run, name)
  end

  def run_node_app_deploy(name)
    ChefSpec::Matchers::ResourceMatcher.new(:node_app_deploy, :run, name)
  end
end  

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: '...'
  )
end  

Conclusion

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.

blogfoster’s vision is to build an ecosystem for bloggers where they can get all the tools and support they need to become successful with their blogs. We use React, Redux, Webpack, SASS, ES6 and more to build an enjoyable platform for thousand of bloggers. Do you want to work with the newest technologies? We are constantly looking for people as passionate as we are. Join our team, let's work together.