Lately I've been working on deploying a multi-tier architecture to Amazon EC2 using Chef. Chef helps you boot and configure infrastructure on the cloud--one line of code to install Apache, PHP, or PostgreSQL for example.
When I first started with Chef I found that there was a pretty high learning curve, and I was in need of a best-practice workflow for building recipes. Through much weeping and gnashing of teeth I've fallen into what I feel is a pretty stable workflow, which is what this post documents.
Installing Chef
I've only done this once; Opscode's quick-start is pretty good with a few caveats...
On the "Install Ruby" step
I'd recommend using RVM. Then you'll need to install a Ruby, and I highly recommend creating a gemset while you're at it:
rvm install 1.8.7-p173
rvm gemset create chef
Then switch to your newly-installed Ruby and gemset. You'll make great use of this later when we get to installing the Chef gems.
On the "Install Chef" step
Skip the "Install Chef" step for a second; we'll get around to it after we install git. [go install git]
Instead of installing the chef gem manually we'll do it in the awesome Rails 3 way: using bundler. Clone my chef repo and set up a .rvmrc
git clone https://github.com/ardell/Chef.git
mv .rvmrc.sample .rvmrc
Now install the bundler gem and bundle install. It'll install all the required Chef gems for you (as defined in Gemfile).
gem install bundler
bundle install
I've taken the liberty of tricking out that repo with a sample .chef/knife.rb file and some other wisdom I've picked up along the way. Copy the sample .chef/knife.rb and customize it with your credentials.
mv .chef/knife.rb.sample .chef/knife.rb
After you successfully get past the "Connect to the Opscode Platform" step, abandon the rest of the getting started guide; it'll just confuse you.
Set up AWS/EC2
You'll need to create a private key on Amazon Web Services since they use keys to authenticate you. Save it in your repo at .chef/[name-of-keypair].pem, you'll use it in the next step. See this blog post for help.
Booting Servers & Building Recipes
Here's where we get to a developer's workflow. The basic process is:
- Import, create, and edit cookbooks
- Add and update roles
- Create or update servers
Lather, rinse repeat. When I'm developing I'll run steps 1, 2, and 3 in order many, many times until the chef run succeeds setting up a perfect server on the initial run.
Step 1: Import, create, and edit cookbooks
Cookbooks are what Chef calls collections of recipes--for example the Apache2 cookbook contains an Apache server recipe and several recipes to install various Apache modules (e.g. mod_ssl, mod_expires). To import a cookbook from Opscode's "blessed" cookbooks, rub the following command where [cookbook-name] is the name of a cookbook such as "apache2". Once you've imported a cookbook it's yours to edit however you like.
knife cookbook site vendor [cookbook-name]
Vendoring a cookbook only imports it into your local repository--chef server does not know anything about it yet. In order for you to use any cookbook on your server you must run:
knife cookbook upload [cookbook-name]
Note that when you vendor a cookbook, knife will actually run git commands without your permission. It will create a branch, pull down the cookbook, switch to master (not your previous branch!!) then merge in changes. I hate that knife does this, and I hope it is changed, but it is what it is.
Step 2: Add and update roles
Each server you boot will be a node, and the right way to assign recipes to nodes is by using roles. In practice roles are like tags, so for a given node you'll have an environment role (e.g. "staging"), and one or more functional roles (e.g. "webserver"). When chef-client runs, it determines which recipes to run based on what roles it has.
Create roles/webserver.rb and add the recipes you want into the run_list declaration. For example for a PHP/Apache role, webserver.rb might look like this:
name "webserver"
description "Apache2/PHP web server."
run_list(
"recipe[postgresql::client]",
"recipe[php]",
"recipe[php::module_pgsql]",
"recipe[apache2]",
"recipe[apache2::mod_php5]"
)
Note that each recipe you reference in your run list will have to be defined (using knife cookbook site vendor [cookbook-name] or created by you), then uploaded using knife cookbook upload [cookbook-name].
When you're done editing your role, and after each time you edit it, you'll need to push those changes to chef server so your nodes can fetch them when they update. Executing the following command will parse the role and add/update it on chef server.
knife role from file webserver.rb
Step 3: Creating and Updating Servers
This is the exciting part. You should now be able to boot a server on ec2 by running the following command... and it's a long command...
knife ec2 server create 'role[webserver]' --ssh-key ec2-keypair --identity-file .chef/ec2-keypair.pem --ssh-user ubuntu --groups default --image ami-88f504e1 --flavor m1.small -Z us-east-1a
Part of the power of Chef is that you don't have to spin up a new server every time you update your recipes. Make sure your changes have been uploaded using knife cookbook upload [cookbook-name] or knife role from file [role].rb, then running the following command will update all the webservers in the staging environment:
knife ssh 'role:staging AND role:webserver' 'sudo chef-client -l debug' -a ec2.public_hostname --ssh-user ubuntu
What this knife command does under the covers is:
- ask chef server to return a list of all webservers in the staging environment
- ssh into each of these servers
- run
sudo chef-client -l debug (log level=debug) on each one
You can use knife ssh to interrogate your servers anytime, but remember you should be using sudo chef-client along with recipes to install packages and recipes in an automated fashion.
Flags for knife ec2 server create
--flavor m1.small is the size of the instance we want
--image ami-480df921 says use the canonical ubuntu image
--ssh-key ec2-keypair is the name of the keypair you created on aws
--ssh-user ubuntu says connect via ssh with the "ubuntu" username (instead of default=root)
--identity-file .chef/ec2-keypair.pem says use the key at this location
-Z us-east-1a Amazon doesn't have capacity of m1.small servers at us-east-1b (which is our default data center), so we use this one instead
-a ec2.public_hostname tells chef to connect to the server using the ec2.public_hostname key, not the fully-qualified domain name (default), which is a address local to ec2
--groups default the user groups that the created user should belong to
Conclusion
I'm learning more about using chef properly each time I use it, but this workflow has been pretty stable for me so far. Do you have a different workflow, or other commands that you prefer?