Automating Node.js deployment to EC2 with Chef

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.

About Michael Wynholds

Mike is the President and CEO of Carbon Five.
This entry was posted in Ops, Web and tagged , , . Bookmark the permalink.

13 Responses to Automating Node.js deployment to EC2 with Chef

  1. Jared Carroll says:

    Thanks for the detailed post Mike. I was overwhelmed when I first started using Chef but it was worth learning. Manual testing was painful though; I’m currently looking into ways of using automated tests.

  2. If you want all the instances in your fleet to stay up to date and avoid running knife ssh every time you want them to converge, add the chef-client cookbook (https://github.com/opscode/cookbooks/tree/master/chef-client) and follow its instructions to create a chef-client role that your nodejs instances can also run.

  3. Gunardi Wu says:

    trying knife ec2 server create as shown above, the client keep trying to connect to localhost:4000 which is not what i set it for (http://something.else.com). I verified the client.rb has the correct chef_server_url, but it keeps going to localhost:4000.Any idea what I’ve gotten wrong ?

    Thanks,

    -Gunardi Wu

    • I think by default it looks in:

      $HOME/.chef/knife.rb

      If you called that file client.rb maybe that’s the issue.

    • Hi again-

      I’m not sure. Are you using RVM by chance? If you are in a directory with a .rvmrc file then $HOME gets re-mapped to that directory, so the .chef directory will be under that.

      Otherwise, can you change some other value in knife.rb that would allow you to see if it is being used at all? It would let you narrow down whether:
      a) you are not using the knife.rb that you think you are
      b) you are using the right knife.rb but there is something wrong within the file

      Also you could post the knife.rb contents here and I can take a look.

      -mike

      • Gunardi Wu says:

        Thanks for the help.

        I am not using RVM.

        Here’s the knife.rb:
        log_level :info
        log_location STDOUT
        node_name ‘gwu’
        client_key ‘/root/.chef/gwu.pem’
        validation_client_name ‘chef-validator’
        validation_key ‘.chef/validation.pem’
        chef_server_url ‘http://po-chef.kdjb10.com’
        cache_type ‘BasicFile’
        cache_options( :path => ‘/root/.chef/checksums’ )
        knife[:aws_access_key_id] = ENV['AWS_ACCESS_KEY']
        knife[:aws_secret_access_key] = ENV['AWS_SECRET_ACCESS_KEY']
        knife[:flavor] = “t1.micro”
        knife[:group] = “default”
        knife[:aws_ssh_key_id] = “po-keypair”

        and I tried:
        knife ec2 server create -x ubuntu -I ami-fd589594 -r ‘recipe[apache2]‘

        and during the client setup:
        ERROR: Connection refused connecting to localhost:4000 for /cookbooks/apache2/..

        Thanks again,

        -gwu

  4. Pingback: Quora

  5. Pingback: nodejs, npm, lessc, django, workflow to production | madteckhead's weblog

  6. Jay G Coding says:

    when one configures the first (master) EC2 instance with node.js or anything else for that matter, isn’t it possible to create as many replicas one needs of this master from AWS console?

    Please don’t take this the wrong way but what am I missing that has me wondering if the process you have described here is actually more labor intensive than it need be?

  7. Ray says:

    hey, i was able to spin up an ec2 instance with node and nginx up and running. but what’s the correct way to load my node/express app on the box? should i use git to pull the source and execute a script (pm2) to start the process? if possible, can you share your recipe for loading the app? thanks a lot!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>