Keeping your Chef DRY
Like many things Ruby, Chef comes with its own DSL which is both a blessing and a curse. It’s great to keep the barriers to entry low (and you can make things look pretty…) but sometimes it makes more advanced techniques quite difficult.
Rails makes some great choices in keeping items (models, controllers) as Ruby classes and not attempting to hide that from the coder but it assumes a level of familiarity with programming that may scare some people (it shouldn’t! There are many great resources online to help people learn) whereas Chef tries to hide the Ruby side of things behind their own DSL.
Keep things DRY
When looking to abstract common code to keep things DRY it’s common
practice to create helper modules and include them when necessary. For example
if you wanted to include the method sort
in both your CommentsController and
PostsController you can do the following:
So now you can use sort
in instances of CommentsController or PostsController
without having to re-define it in each. Awesome.
The problem with Chef recipes
That’s great for platforms like Rails that separate each controller or model in
to its own class. You can mix in behaviour to the controllers that need it
while not worrying about polluting the ones that do not. Chef recipes,
unfortunately, are all instances of the same Chef::Recipe
class.
The example provided by the official windows cookbook on how to include
helper libraries in to your recipes is to use
::Chef::Recipe.send(:include, Windows::Helper)
.
I don’t like this for two reasons:
- It uses
#send
to bypass restrictions on calling the private method#include
- It monkey patches ALL instances of the Chef::Recipe class so you risk modifying behaviour of other recipes in the run list if they have a different definition of the same method.
Here’s an example:
Then by running chef-client -z -r with_library,without_library
we get the
following output:
Compiling Cookbooks...
with_library #<Chef::Recipe:0x57ff480>: [:example_method]
Do stuff here
without_library #<Chef::Recipe:0x59dfc78>: [:example_method]
Do stuff here
Converging 0 resources
So we can see that our Chef::Recipe
instances have two different object IDs
(0x57ff480 and 0x59dfc78 here, yours may differ) yet they both have
#example_method
instance methods. Oops. Even worse, if you change the run
list order to be without_library,with_library it’ll throw an exception:
NameError
---------
No resource, method, or local variable named `example_method' for
`Chef::Recipe "default"'
So you’re now in the situation that your recipes behave differently depending on their run order.
A different way
In order to workaround this, I use Object#extend in my Chef recipe:
Now we get the same behaviour for both run orders, and we’ve only included the
helper method where it’s really needed. Now our chef-client
run produces the
following:
Compiling Cookbooks...
with_library #<Chef::Recipe:0x44cc448>: [:example_method]
Do stuff here
without_library #<Chef::Recipe:0x53d7530>: []
Converging 0 resources
So now we’re consistent across run orders and haven’t risked overwriting methods used in all recipes. Woohoo.