Chef runs from your laptop via ssh using chef-provisioning-ssh

19 May 2015

When comparing configuration management systems, one of the biggest selling points of ansible is how easy it is to get started in a small environment - you type up a recipe/playbook on your laptop, run ansible, and it connects via SSH to your machines to configure them. This blog post shows you how to do the same thing with chef using chef local mode, chef-provisioning and chef-provisioning-ssh. This setup can be very useful if you have just a couple of machines, don’t want to set up a chef server until you grow your infrastructure, and want to orchestrate everything from your laptop.

Steps

If you haven’t already, install the latest ChefDK, which will install chef, chef-provisioning and lots of other goodies. It doesn’t however install chef-provisioning-ssh, so you’ll have to do that separately:

chef gem install chef-provisioning-ssh

Next, create your chef repository as normal. I use the chef generate commands here to speed things up a little:

chef generate repo chef-repo
cd chef-repo/cookbooks
chef generate cookbook my-cookbook
echo 'log "Hello world"' >> cookbooks/my-cookbook/recipes/default.rb
cd ..

Next, create a file called site.rb in the root of the chef repository. This is the script you will run to run chef across your infrastructure. In this example, we have just one machine:

#!/usr/bin/env chef-client -z
require 'chef/provisioning/ssh_driver'

with_chef_local_server({
  :chef_repo_path => File.dirname(__FILE__),
  :cookbook_path => ["#{File.dirname(__FILE__)}/cookbooks",
                     "#{File.dirname(__FILE__)}/berks-cookbooks"]
})

with_driver 'ssh'

machine 'mymachine' do
  converge true
  recipe 'my-cookbook'
  #role 'base'
  machine_options :transport_options => {
    :host => 'mymachine.int.example.com',
    :username => 'mark',
    :ssh_options => {
      :use_agent => true
    }
  }
end

Let’s break this down:

#!/usr/bin/env chef-client -z

The shebang line here simply lets you run the script directly using ./site.rb and it will chef-client in local mode. This works because you can pass a recipe file directly to chef-client -z or chef-apply and it will be run.

require 'chef/provisioning/ssh_driver'

This is just a ruby require to load chef-provisioning-ssh.

with_chef_local_server({
  :chef_repo_path => File.dirname(__FILE__),
  :cookbook_path => ["#{File.dirname(__FILE__)}/cookbooks",
                     "#{File.dirname(__FILE__)}/berks-cookbooks"]
})

This tells chef-provisioning to use chef local mode when provisioning the server, with the chef repository in the current directory. This is what lets you provision the remote machine with just the configuration repository located on your laptop.

The cookbook_path parameter is optional, but I added an additional berks-cookbooks path for all cookbooks installed via berkshelf. I run berks vendor berks-cookbooks to install any community cookbooks or cookbooks with their own repository to a separate directory.

with_driver 'ssh'

Use the ssh driver for all subsequent connections.

machine 'mymachine' do
  converge true

Now we are into the machine definition itself. You have one of these for each machine you want to provision, and if desired you can use regular ruby to provision many machines at once using loops.

The converge true line says to run chef every time we run the site.rb script. Chef provisioning is intended for the initial bootstrapping of a machine, and the machine would take over its own running of chef as a daemon or via cron. Without this option, chef-provisioning would only run chef on the initial run when the machine is first provisioned.

recipe 'my-cookbook'
#role 'base'

Specify the run list for your machine here. I specified the simple recipe created earlier for this example, but you would probably use roles for a real installation as in the commented out line. Use multiple role or recipe lines as needed.

  machine_options :transport_options => {
    :host => 'mymachine.int.example.com',
    :username => 'mark',
    :ssh_options => {
      :use_agent => true
    }
  }

Next is the various ssh options for the machine. There are lots of options you can use, and they are documented in the chef-provisioning docs, but What I’ve specified here is probably the minimum you will want to set - the machine’s hostname, the username to connect as, and instructing it to use the ssh agent. You can use password based authentication here too, but that would mean hard coding the password into the recipe, which is probably a bad idea.

Once you have your site.rb created, chmod +x site.rb to make it executable, and then run it:

$ ./site.rb
Starting Chef Client, version 12.0.3
resolving cookbooks for run list: []
Synchronizing Cookbooks:
Compiling Cookbooks...
[2015-05-09T00:03:35-04:00] WARN: Node laptop has an empty run list.
Converging 1 resources
Recipe: @recipe_files::/Users/mark/git/chef-repo/site.rb
  * machine[mymachine] action converge
    [mymachine] Starting Chef Client, version 12.2.0
           resolving cookbooks for run list: ["mh-base::test"]
           Synchronizing Cookbooks:
             - my-cookbook
           Compiling Cookbooks...
           Converging 1 resources
           Recipe: my-cookbook::default
             * log[Hello world] action write


           Running handlers:
           Running handlers complete
           Chef Client finished, 1/1 resources updated in 5.050801491 seconds
    - run 'chef-client -l auto' on mymachine

Running handlers:
Running handlers complete
Chef Client finished, 1/1 resources updated in 20.512658 seconds

And there you have it. Chef runs orchestrated entirely from your laptop. Chef-provisioning will automatically use sudo, so as long as you have key based ssh access, and your user on the remote machine has sudo access, things will just work.

Caveats

There are a few things to be aware of with this setup, mostly surrounding the fact that there is a chef-zero server running under the hood, and the fact that it keeps some saved state.

First, if you change how you connect to the server, chef-provisioning will continue to try to use the old cached credentials to connect. This type of situation is common if you initially bootstrap a machine using the pi or ubuntu users, and then delete them and connect using a regular user instead.

To fix this, delete ~/.chef/provisioning/ssh/mymachine.json, and chef-provisioning will use the new connection parameters on the next run.

Similarly, any modifications to the run list in site.rb are additive. Chef-provisioning doesn’t wipe away any existing run list on the node when it runs. This means that if you want to remove a run list entry, you should do it with knife from inside the chef repository once you have fixed site.rb:

knife node run_list remove mymachine 'recipe[my-cookbook]' -z

This is exactly the same as regular knife commands, but you add the -z option. I find it easiest to just put it on the end of the command, as you get an error if the option appears first because knife is expecting to see a command there.

What’s going on here?

Chef-provisioning is intended to let you quickly get machines up and running entirely using chef. It essentially does four things:

Chef-provisioning-ssh is a driver for chef-provisioning that lets you configure machines for ssh, which means it skips step one above - the machine is already created, so nothing needs to be done. Conveniently, the other three steps are exactly what’s needed to complete a chef run from scratch, and that’s exactly what we abuse chef provisioning to do.

The next part of the magic is chef-zero, or chef local mode. This lets you start a chef server on your laptop pointing to a local repository on disk, without any prior setup, and the server goes away once the chef run is complete. Chef-provisioning also sets up port forwarding over ssh so that the remote server can talk to the chef server on your laptop even if there is no direct connection back.

The beauty of this approach is, if your infrastructure grows large enough to require a chef server, you just upload the contents of the repository to the chef server, update the client config on the server (using chef of course), and you’re up and running on a real chef server with no other changes to your clients.

While there are a few issues that need to be worked out (and which I’m working on), this approach is serving me well for my home setup, and it’s incredibly simple to get started with.