Working With Multiple Chef Orgs

The Problem

It’s probably quite common that a lot of developers or operations engineers will typically work with a couple of Chef organisations. These organisations might represent the infrastructure configurations for different products; or perhaps a development and production environment, though this scenario is more often accomplished through the use of different Chef environments. But what if the nature of your work means you’re dealing with many Chef organisations at a time? It could become quite cumbersome managing individual working copies of different repositories representing the data you store in various Chef orgs; not to mention maintaining subtly different Knife configurations, and sets of user and validator PEM keys. So here’s my solution to this problem.

The Solution

The Knife client will search in different standard locations until it finds a valid

knife.rb file.

You might be used to managing an unversioned .chef directory in the root of your Chef repository working copy. Although you might exclude this via .gitignore it’s very easy to accidentally add it, and in so doing, you’ll inadvertently add your Chef user PEM key, and the validation key that lets nodes register with your Chef server! A better approach is to manage your various Knife configurations in a central location, say ~/.chef. You can version control those aspects that aren’t private, and all you need to do is drop the private files such as PEM keys in place.

I have a few utility functions in my ~/.zshrc file (you use Oh-My-ZSH, right?!). I can use kitchens to list the environments that I can work with; and I can use cookwith <env_name> to work on a particular environment.

kitchens() {
    ls -1A ~/.chef | grep -vi knife
}

The kitchens function above simply lists the directory names that exist in my ~/.chef directory. Each of those directories contains a structure like the one below:

╭─max@hedgehog  ~/.chef/maxmanders ‹ruby-2.1.5›
╰─$ pwd
/Users/max/.chef/maxmanders
╭─max@hedgehog  ~/.chef/maxmanders ‹ruby-2.1.5›
╰─$ tree
.
├── config.yml
├── maxmanders-validator.pem
└── maxmanders.pem

0 directories, 3 files

As you can see, each Chef org directory contains a YAML configuration file, a user PEM key, and the validator key for that org. The configuration file is parsed by my dynamic ~/chef/knife.rb:

CHEF_ENV = ENV["CHEF_ENV"] || "maxmanders"
current_dir = File.dirname(__FILE__)
env_config = YAML.load_file("#{current_dir}/#{CHEF_ENV}/config.yml")
cookbook_dir = "#{ENV["HOME"]}/chef/#{CHEF_ENV}"
 
log_level                :info
log_location             STDOUT
node_name                env_config["node_name"]
client_key               "#{current_dir}/#{CHEF_ENV}/#{env_config["node_name"]}.pem"
validation_client_name   env_config["validator"] || "chef-validator"
validation_key           "#{current_dir}/#{CHEF_ENV}/#{env_config["validator"]}.pem"
chef_server_url          env_config["server"]
cache_type               'BasicFile'
cache_options( :path => "#{current_dir}/#{CHEF_ENV}/checksums" )
cookbook_path            env_config["cookbook_paths"].collect { |x| "#{cookbook_dir}/#{x}" }
ssl_verify_mode          :verify_peer

This knife.rb file would be used to read a file named config.yml that might look something like this:

node_name: "maxmanders"
server: "https://api.opscode.com/organizations/maxmanders"
validator: "maxmanders-validator"
cookbook_paths:
  - "cookbooks"
  - "site-cookbooks"

I start off by setting the value of the CHEF_ENV I’m working with, based on the presence of an environment variable named CHEF_ENV; if I don’t have that environment set, I’ll default to my personal Chef org. I then use Ruby’s YAML module to parse the config file found in the directory described earlier, based on CHEF_ENV. The most important things that afford the flexibility of working with multiple orgs are the locations of my PEM keys, and the locations of my local cookbooks.

Once I have multiple directories in ~/.chef/, all being listed when I run kitchens, then I can use cookwith, below, to switch between them!

cookwith() {
    local chef_env=$1
    export CHEF_ENV=${chef_env}
    cd ~/chef/${chef_env}
}

At the end of this function, I change directory to a directory of the same name as my chosen CHEF_ENV located in ~/chef/. This is where I store all of my repository working copies. For a lot of purposes, the local working copies aren’t that important; I might just be querying the Chef server with knife. In these cases, I can just use a symlink to some generic repository working copy.