Here at Opscode, we think of the Chef recipe DSL, or domain-specific language, as being quite straightforward to learn. Despite the fact that under the hood it’s all Ruby, you most definitely do not need to know Ruby to use Chef. Your humble correspondent would not consider himself a Ruby programmer by any stretch, but he knows enough of the Chef DSL and basic Ruby to be able to help customers automate their infrastructure.
However, there are some curious idioms that sometimes appear in recipe code that are worth explaining. You can think of these as Ruby poking her friendly head out from behind the curtain of the DSL. Here are the most common ones explained.
::Chef::Recipe.send
Occasionally in recipe code you may see something like ::Chef::Recipe.send(:include, Opscode::OpenSSL::Password). What does this mean?
Chef has two phases of execution: a compilation phase and an execution phase. In the compilation phase, the recipes are evaluated as Ruby code, and resources are added to the resource collection. Next, in the execution phase, Chef runs the appropriate provider’s action on each resource. If you’re an object-oriented programmer, you can think of the provider as being the implementation; the resource is the interface. Chef uses the Ohai data to determine what provider to choose for a given resource.
In normal, non-Chef Ruby, you can include other classes in your own by using the include syntax; for example, include Opscode::OpenSSL::Password. Used in a recipe, however, the recipe compiler will try to find a provider for a resource of type “include”. Clearly, this would fail; hence the method shown here, which will use a little bit of Ruby metaprogramming to include the requisite OpenSSL utility class.
What’s with the ::Chef and ::File instead of Chef and File?
In some recipes, you may see ::File used to refer to the Ruby File class. Why isn’t it just “File”?
The reason is because Chef has its own classes named File. Specifically, there are resource and provider classes within Chef called File. To avoid ambiguity — ambiguity leads to errors — whenever we want to refer to Ruby’s File class, we use ::File.
Resource Declarations with end.run_action(:something)
Some recipes have resource declarations that look normal except for something weird: end.run_action(:some_action). They look like this:
package "foo" do action :nothing end.run_action(:install)
This is an indication that the recipe author wants to run the package[foo] resource’s :install action during Chef’s compilation phase. This might be because the resource’s action is required as a pre-requisite for the execution phase. One example is installing certain Ruby gems into Chef itself so that the execution phase can use them. This is such a common use case that we invented a resource, chef_gem, that does exactly this. It replaces older code that looked like this:
gem_package "foo" do action :nothing end.run_action(:install) Gem.clear_paths
which could be a bit tough to understand, especially with the Gem.clear_paths call (used to tell the Rubygems framework about the newly-installed gem).
def initialize inside LWRPs resource declarations
In some older cookbooks, resource declarations often have a funny method definition like
def initialize(*args) super @action = :create end
The LWRP framework makes it easy to create new subclasses of Chef::Resource and Chef::Provider without actually writing Ruby code, just like how Chef recipes are really a way for you to write Chef::Recipe classes, all fronted by a nice DSL. So what’s this initialize method all about?
Prior to Chef 11, there was no way to set the default action for an LWRP in the DSL: the default_action syntax did not exist yet. This bit of idiomatic Ruby ensured that when the DSL was converted to a real Chef::Resource subclass object and instantiated, that the superclass’s initializer would be called with the default action of :create.
As you can see, we’ve now made it infinitely easier to set default actions for your custom resources!
Chef::Log in recipes versus the log resource
Occasionally you may see people using Chef::Log statements in recipe code rather than using the log resource. There is no functional difference between the two; they will both log to Chef’s logger, whose output is generally sent to a file like /var/log/chef/client.log on UNIX/Linux. So why prefer the former versus the latter?
First, we suspect that sometimes this is the result of copy and paste from other people’s recipes. Nevertheless, there are some good reasons to use Chef::Log; for one, it means that log statements don’t actually create a resource in the resource collection. The log resource will always register as a resource that is updated; in other words, the :write action always runs. Sometimes this may not be desirable, particularly if you consider more-than-zero resources updated during normal operation an exceptional condition.
Wrapping Up
We like to think that Chef’s various DSLs allow you to express your infrastructure in a natural way that is self-documenting. No system is perfect, however, which is why you may occasionally see Ruby making a cameo in DSL code. We hope that this post has explained some of the common idioms so that you can become better-informed Chefs!