Reducing knobs and switches in Chef

You're reusing community cookbooks? Great!

But say you have some behaviour you want to be able to turn on or off that involves setting several properties spread across those cookbooks.

One way you could do that is put something like this in a role or environment:

{
  "cookbook_one": {
    "a": "changed value 1",
    "b": "changed value 2",
    "c": "changed value 3"
  }
}

and then, when you want to turn the behaviour off, do this:

{
  "cookbook_one": {
    "a": "regular value 1",
    "b": "regular value 2",
    "c": "regular value 3"
  }
}

Make sure to document that somewhere, though. Maybe right in your role? Nope. Sorry. They're in JSON, which isn't too friendly towards comments. You'll have to document it somewhere else, unless you want to commit to always updating roles and environments with knife role|environment from file RUBY_FILE

Unfortunately people can still change those roles and environments through the web interface or knife role/environment edit commands, causing your file to become out of sync with what's really being used, but perhaps you could send a memo around asking everyone not to do that.

Whatever works.

Except, it's kind of sad that you've taken this huge step to convert your infrastructure to code, yet you're back to creating documentation about a bunch of manual steps you need to take to do something or sending memos around telling people to not do certain things the system allows them to do.

Getting rid of unnecessary complexity

Why can't you reflect the fact that you want to change these three values in one way when the behaviour is "on" and in another when it's "off", and that you never want to change them in any other way than that? This would bake the true knowledge of what those settings accomplish to your organization (not their purpose to the community cookbook developer) into code, which is what you really want, right?

Wouldn't it be great if you could do this in a role/environment to turn the behaviour on:

{
    "some_behaviour": "on"
}

and this to turn it off:

{
    "some_behaviour": "off"
}

???

Well, you can! And wrapper cookbooks, along with being great for many other reasons, will allow you to do just this. Just create a cookbook with the following in attributes/default.rb:

if node[:some_behaviour] == "on"  
  default[:cookbook_one][:a] = "changed value 1"
  default[:cookbook_two][:b] = "changed value 2"
  default[:cookbook_one][:c] = "changed value 3"
else  
  default[:cookbook_one][:a] = "regular value 1"
  default[:cookbook_two][:b] = "regular value 2"
  default[:cookbook_one][:c] = "regular value 3"
end  

and either include it in your run_list before the other cookbooks, or have it completely wrap them by adding the following in recipes/default.rb:

include_recipe "cookbook_one"  
include_recipe "cookbook_two"  

(Note: You also have to add "depends" statements on both of those in your metadata.rb file).

Except...

If any of those values that you turn on/off in your wrapper cookbook are part of other composed attributes in those community cookbooks (for example:

default[:cookbook_one][:d] = "#{node[:cookbook_one][:a]} and #{node[:cookbook_one][:c]}"  

), you may very well run into trouble unless you also redefine them in your wrapper cookbook. And it looks like this problem probably won't be fixed.

But don't despair. You can work around it by putting this somewhere in your recipe chain:

# maybe in a lib file
class Chef::Recipe  
  def recompute_all_attributes!
    node.run_context.loaded_attributes.each do | cookbook_name |
      node.from_file(
        node.run_context.resolve_attribute(
          *node.parse_attribute_file_spec(cookbook_name)))
  end
end

# in your recipe before you start using any of the attributes
recompute_all_attributes!  

Basically, it makes Chef load all of your attributes defined in cookbooks once again. As long as you're not mixing levels too much (I stick generally to default in recipe attribute files and override in environment/role files, and let Chef's basic precedence rules take it from there), this will result in those composed attributes correctly noticing that the attributes they depend on have been redefined by the wrapper cookbook.

The future of wrapper cookbooks...

From the comments on this bug report, I gather that the Chef folks expect versioned roles and environments to solve all of the problems that led people to use wrapper cookbooks. I'm skeptical of that. Lack of versioning is definitely something that encouraged me to think more in terms of wrapping cookbooks I used. But I can't see how they'd help you reduce your knobs and switches.

A lot of what we do in software development and operations is figure out the right place to put complexity. We never get rid of it. We just delay it until it matters. I believe the right place for the complexity of setting multiple attributes to accomplish a single thing is in a wrapper recipe. It's hard to imagine how roles and environments could accomplish the same goals.

Consider the perspective of someone new to your project. Most likely, the most important thing for her to know is how to turn that behaviour on/off. There you go. One simple setting. But maybe later, something needs to be changed about that. Well, just search for that setting in the recipe and she sees what it's really doing. She didn't need to know that before, but it's not hard to find when she does.

I'm calling this "just in time complexity", and I think it's pretty important, especially when others have to figure out what the hell your infrastructure code does. Hopefully this issue gets resolved one way or the other, but until then, you can use that hack to get behaviour you're probably expecting anyway.