Opinionated Programmer - Jo Liss's musings on enlightened software development.

Chef Solo tutorial: Managing a single server with Chef

The Chef documentation assumes you have an entire server farm to manage, so it hits you with a lot of complexity. If all you want is to set up and maintain a single VM, this tutorial will help.

This is the cloud

We will be creating “throwaway” VMs that we can recreate with a single call. If you need to have a single server system persist for years across all changes, you should perhaps check out Puppet for a less cloudy solution.

Using Amazon EC2?

Any fresh instance of an Ubuntu AMI will do, for example ami-06ad526f (11.04, 32-bit EBS in us-east-1).

Hint: The EC2 API tools are (by nature) complex and error-prone, so I recommend you stay with the point-and-click web interface for as long as possible, instead of trying to automate launching new instances.

Hint 2: Create a separate EBS volume as a data partition to hold your databases, etc.

Hint 3: Use Elastic IPs for all your instances.

All you need is

  • a laptop with Bash and SSH (no need to even install Chef), and
  • any vanilla Linux server that you can SSH into.

Why do we run against vanilla servers? In my opinion, the system your deployment scripts run against should be as minimal possible, essentially straight from the vendor. So instead of manually installing packages like Chef and then creating a snapshot to instantiate our VMs from, we will use a freshly installed system and make the entire process scripted. On the downside, we need some bootstrapping code (~30 LOC), but on the upside we get more repeatability and less maintenance overhead, and we stay independent from any specific cloud or virtualization provider.

Overview

Since we only have one server to manage, using Chef Server and Chef Clients is clearly overkill. We will use Chef Solo instead. In a newly created directory on our laptop, we will have the following files (all under version control if you like):

1
2
3
4
5
6
deploy.sh     <--- run "./deploy.sh" on your laptop to deploy
install.sh    <--- this is run on the server to bootstrap and call chef-solo
solo.json     <--- chef configuration
solo.rb       <--- chef configuration
cookbooks/op/recipes/default.rb   <--- the most important file -- your
                                       server recipe goes here

That’s all we need to turn any bare-bones Linux system into a fully equipped server. Let’s now take a look at the contents of each file. You can copy and paste freely (everything was written by me, and it’s in the public domain). Some of it assumes Ubuntu – adjust accordingly.

deploy.sh

The deploy.sh script is the one you run on your laptop every time you want to (re-)apply your Chef recipe to your server. It only copies the tree to the server and runs install.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash

# Usage: ./deploy.sh [host]

host="${1:-ubuntu@opinionatedprogrammer.com}"

# The host key might change when we instantiate a new VM, so
# we remove (-R) the old host key from known_hosts
ssh-keygen -R "${host#*@}" 2> /dev/null

tar cj . | ssh -o 'StrictHostKeyChecking no' "$host" '
sudo rm -rf ~/chef &&
mkdir ~/chef &&
cd ~/chef &&
tar xj &&
sudo bash install.sh'

(Remember to change the default host name to match yours.)

install.sh

install.sh is responsible for bootstrapping the system if necessary (installing Chef), and then calling the chef-solo binary. (You don’t ever need to run it manually. Keep it mode 644 as a precaution against wrecking your development machine.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash

# This runs as root on the server

chef_binary=/var/lib/gems/1.9.1/bin/chef-solo

# Are we on a vanilla system?
if ! test -f "$chef_binary"; then
    export DEBIAN_FRONTEND=noninteractive
    # Upgrade headlessly (this is only safe-ish on vanilla systems)
    aptitude update &&
    apt-get -o Dpkg::Options::="--force-confnew" \
        --force-yes -fuy dist-upgrade &&
    # Install Ruby and Chef
    aptitude install -y ruby1.9.1 ruby1.9.1-dev make &&
    sudo gem1.9.1 install --no-rdoc --no-ri chef --version 0.10.0
fi &&

"$chef_binary" -c solo.rb -j solo.json

solo.rb

solo.rb only sets two paths for Chef Solo. You should go with this:

1
2
3
4
root = File.absolute_path(File.dirname(__FILE__))

file_cache_path root
cookbook_path root + '/cookbooks'

solo.json

solo.json holds a pointer to the recipe(s) we want to run (that’s only one recipe for now). My cookbook is called “op” (for Opinionated Programmer) – you should name yours after your server or your site.

1
2
3
{
    "run_list": [ "recipe[op::default]" ]
}

cookbooks/op/recipes/default.rb

I like to just have a single “default” recipe that holds all the server configuration. If it gets out of hand, you can always split it up, but for small sites, a single recipe file should be easiest to deal with.

A recipe is simply a list of resources. The Resources page on the Chef wiki is probably the one piece of documentation you will be consulting a lot.

I will give you a few examples of things you might want to do in your recipe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# --- Install packages we need ---
package 'ntp'
package 'sysstat'
package 'apache2'

# --- Add the data partition ---
directory '/mnt/data_joliss'

mount '/mnt/data_joliss' do
  action [:mount, :enable]  # mount and add to fstab
  device 'data_joliss'
  device_type :label
  options 'noatime,errors=remount-ro'
end

# --- Set host name ---
# Note how this is plain Ruby code, so we can define variables to
# DRY up our code:
hostname = 'opinionatedprogrammer.com'

file '/etc/hostname' do
  content "#{hostname}\n"
end

service 'hostname' do
  action :restart
end

file '/etc/hosts' do
  content "127.0.0.1 localhost #{hostname}\n"
end

# --- Deploy a configuration file ---
# For longer files, when using 'content "..."' becomes too
# cumbersome, we can resort to deploying separate files:
cookbook_file '/etc/apache2/apache2.conf'
# This will copy cookbooks/op/files/default/apache2.conf (which
# you'll have to create yourself) into place. Whenever you edit
# that file, simply run "./deploy.sh" to copy it to the server.

service 'apache2' do
  action :restart
end

Note that all dependencies are implicitly given by the ordering of the resources.

This should be enough examples to get you going. Again, all of these resources (and many more) are documented on the Resources page.

Hints

  • As you write your recipe, re-run ./deploy.sh frequently, like you would recompile a piece of software.
  • When you are done, tear down your VM and redeploy on a fresh system, just to make sure everything is working OK. Sometimes subtle dependency/ordering issues creep in that are only revealed by applying the entire recipe on a vanilla system. You don’t want to be left with a non-working recipe when your server goes down and you need to re-deploy afresh.

All code was written by me and is licensed under CC0 (public domain).