Automating Node.js deployment to EC2 with Chef

Michael Wynholds ·

This post is a follow up to my colleague Ben’s excellent post from three weeks ago describing how he got node.js up and running on Amazon EC2. He asked me if I could automate his deploy in order to help him provision his production environment. I decided it was a good opportunity to learn Chef.

The Basics

Before we get started, you need to be familiar with the various components of Chef, and how they work together. Chef can be used in two ways:

Chef Solo

Chef Solo is a command-line tool that allows you to run Chef locally using cookbooks that are on your local file system. This is great to play around with, but it requires that you a) have an existing server (or VM) to run on, and b) manually install Ruby, RubyGems and Chef on that server, and copy any necessary cookbooks to it.

If you are trying to provision a bunch of servers from a cloud provider like EC2, then all of that manual work kind of kills your productivity.

Chef Client-Server

Chef Client-Server to the rescue! This architecture is definitely more complicated, but once you are up and running it is much easier to manage, and allows you to provision new servers in a much more automated way.

There are three players in the Chef client-server world:

  • Chef Server – This is a web app running the Chef Server API. It hosts all of your cookbooks and other Chef resources.
  • Chef Client – This is the machine or VM that you are provisioning. These are called “nodes” in Chef parlance. Nodes make calls to the Chef Server API to pull down cookbooks and other Chef resources.
  • Knife – This is the box where you initiate Chef-based installs, and manage resources on the Chef Server. Typically it’s your development machine, but it can be any box (or several). It’s called “Knife” because you use a command-line tool called knife.

Let’s Get Started

Ok, let’s get on to the business of automating deploys of node.js…

Step 1 – Configure Chef Server

The first thing we need is a Chef Server. The easiest thing to do is to sign up for a free account with Opscode’s Hosted Chef. If you continue to use Chef in your normal business operations, you will quickly outgrow the free account. Your other option, is to install your own Chef Server, which is what I eventually did.

Step 2 – Install Knife

Once you have your Chef Server up and running, you need to set up Knife on your development box. This is easy because knife comes with the chef ruby gem:

gem install chef

You can now run knife to see its available sub-commands.

Knife has a plugin architecture which allows hosting providers to extend knife. Since we’re deploying to EC2, let’s install the knife-ec2 plugin:

gem install knife-ec2
gem install net-ssh-multi

(I needed to manually install the net-ssh-multi gem, although I wonder why it’s not just a dependency)

If you run knife again, you will notice the new “ec2” sub-command that is available.

Step 3 – Configure Knife for Chef Server

Now let’s configure knife to communicate with our Chef Server. If you are using Hosted Chef, it’s pretty straightforward. First, generate a private key from your user profile page. Then, create an organization and generate a validation key from the Organizations page. Finally, generate a knife.rb configuration file from the Organizations page. Download your private key, you organization’s validation key and your knife.rb file and copy them to ~/.chef. Your knife.rb will look something like this:

current_dir = File.dirname(__FILE__)
log_level                :info
log_location             STDOUT
node_name                "mwynholds"
client_key               "#{current_dir}/username.pem"
validation_client_name   "yourorg-validator"
validation_key           "#{current_dir}/yourorg-validator.pem"
chef_server_url          "https://api.opscode.com/organizations/yourorg"
cache_type               'BasicFile'
cache_options( :path => "#{ENV['HOME']}/.chef/checksums" )
cookbook_path            ["#{current_dir}/../cookbooks"]

Now you can test out your configuration by running knife client list. If it doesn’t give you an error message, then you’re good!

Step 4 – Upload Cookbooks

You need to upload any cookbooks you want to use to your Chef Server. You can do this with knife, but first you need to find (or write) some cookbooks. Luckily, there are lots of existing cookbooks out there, and for the purpose of installing node.js, all of the necessary cookbooks exist.

Many useful cookbooks exist in git repositories on GitHub. My technique has been to fork and clone repos to my development box. Here are the two that I have so far:

In order to upload cookbooks, you first need to configure knife to know where cookbooks live locally. To do that you can edit the last line in your knife.rb to look more like this:

cookbook_path ["/Users/mike/Projects/opscode-cookbooks",
               "/Users/mike/Projects/mdxp-cookbooks"]

Change the paths to point to your local repos. Then upload the necessary cookbooks:

knife cookbook upload apt build-essentials runit git nodejs

Running knife cookbook list should list those five cookbooks.

Step 5 – Create a Role

When provisioning a node with Chef, you will be applying multiple cookbook recipes to your new node. Group these recipes together into a role to make the knife commands easier. You can create a “nodejs” role like this:

knife role create nodejs

This will open up your editor with the default role JSON. You need to fill in the run_list and override_attributes so that it looks like this:

{
  "name": "nodejs",
  "description": "",
  "json_class": "Chef::Role",
  "default_attributes": {
  },
  "override_attributes": {
    "nodejs": {
      "version": "0.4.11",
      "npm": "1.0.30"
    }
  },
  "chef_type": "role",
  "run_list": [
    "recipe[apt]",
    "recipe[build-essential]",
    "recipe[git]",
    "recipe[nodejs]",
    "recipe[nodejs::npm]"
  ]
}

Verify that it worked using knife:

knife role show nodejs

You should see the JSON above.

Step 6 – Configure Knife for EC2

Now that you have all of the recipes and roles configured on the Chef Server, it’s time to get Knife talking to EC2. You need to have an AWS account, so if you don’t have one, sign up now. Once you have done that, update your knife.rb file with the necessary configuration. I added the following lines to my knife.rb:

knife[:aws_access_key_id] = '<my access key id>'
knife[:aws_secret_access_key] = '<my access key secret>'
knife[:flavor] = 't1.micro'
knife[:image] = 'ami-63be790a'
knife[:aws_ssh_key_id] = 'mikebp'
knife[:identity_file] = "/Users/mike/.ssh/keys/mikebp.pem"

Edit this file to reflect your AWS credentials. I also added a default :flavor and :image value. You can also pass this info on the command line.

Add the private key to your SSH authentication agent to allow you to log in to your new EC2 instances without having to pass it on the command line, and use knife to issue SSH commands.

ssh-add /Users/mike/.ssh/keys/mikebp.pem

Step 7 – Create your instance

Wow! Are we actually ready to create a new node.js server? We are… and here’s all it takes:

knife ec2 server create -x ubuntu -r "role[nodejs]"

Pro-Tip: “-x ubuntu” just tells knife to log in to the server as the user “ubuntu”. This is the user that exists on the AMI that we are using. You will want to change this to reflect your specific AMI.

You should now see a bunch of commands scroll by, and it will take 20 minutes or so. (most of that is building node.js from scratch). When all is said and done, you should have a new EC2 micro instance running node.js! You can get some info about the instance with knife.

knife ec2 server list

Step 8 – Now what?

Now you should have a node.js server up and running on EC2. Or, better yet… you now have a whole cluster of them! But what happens if you want to make a change to your recipes and re-run them on all of the nodes in your cluster?

When Chef provisions a node, it stores a copy of the configuration for that node, including what it needs to query Chef Server for any updates to data there (cookbooks, recipes, roles, etc). All you need to do to re-run your run list is to log in to that node and run chef-client. Sounds easy, right?

But wait… what if I have 100 nodes in my cluster and I want to update all of them? That sounds time-consuming.

Knife to the rescue! First make any updates to your cookbooks and upload them to Chef Server. Then use knife to re-provision your nodes.

knife ssh 'role:nodejs' -a ec2.public_hostname -x ubuntu 'sudo chef-client'

This command will search all of your managed nodes (which are cataloged on the Chef Server) for the ones with a role of “nodejs”, which (as you probably remember) is what we named the role that we gave them. It logs into each one (again, as the user “ubuntu”) and runs chef-client as root; this updates its cookbooks from Chef Server and runs them.

Conclusion

I realize the whole thing is a little daunting if it’s your first time with Chef. But if you’ve gotten this far, you are pretty well set up to provision instances at will. It’s just a matter of managing your cookbooks and roles.

There is a lot more to learn about Chef than we’ve gone over here, but at this point you have the basics down.

Now let’s cook.

Michael Wynholds
Michael Wynholds

Mike is the President and CEO of Carbon Five.