The Recipe DSL

Developed with
Start course
2h 12m

In this lesson, we will dig deeper into Recipes, and talk about the recipe syntax.

We will discuss the Ruby-based domain specific language, which is usually abbreviated as DSL.

You will start with the basic pattern for working with resources before moving into some of the Ruby code.

We will cover the anatomy of a resource, a Ruby block. You will learn the four components: the resource type, the name, one or more attributes, and one or more actions.

At this point, we will begin some coding in Ruby to see how it all works. Some of the examples will be basic to get you familiar, and then we will add more attributes and actions to expose all that the resources can do in the recipes.
We will also look at some of the methods provided by the DSL to make writing recipes a bit easier: platform and platform_family, value_for_platform_family, include_recipe, attribute, data_bag and data_bag_item, and shell_out.

Finally, we will provide a quick test to see what you have been able to retain from the lesson.


Welcome back! In the previous lesson I covered the components of a Cookbook. The component I called out as being the most important was the recipe. So in this lesson I want to dig deeper into Recipes, and talk about the recipe syntax.

Chef uses a Ruby based domain specific language, which is usually abbreviated as “DSL.” A DSL is a language used for a specific purpose. And in the case of Chef, the recipe DSL provides a consistent way to work with resources.

I want to start the lesson with the basic pattern for working with resources before moving into some of the Ruby code. I’ve covered resources at a very high level, however since it’s almost time to start writing some code, it’s worth going more in depth.

Here’s the anatomy of a resource. A resource is just a Ruby block. And if you’re not familiar with Ruby blocks, that’s okay. It’s probably worth you taking the time to learn more about Ruby and blocks outside of this course; however, for now just consider a block to be a section of code responsible for configuring a resource.

Resources have 4 components. The resource type, the name, and then one or more attributes, and one or more actions. Most attributes will have a default value, which means you’re only required to use an attribute if you want to change the default.

Actions will always have a default. So again, if the default value is what you need then you aren’t required to specify it.

type 'name' do
attribute 'value'
action :type_of_action

Here’s a few examples.
package 'tree' do
version '1.7.0-3'
action :install

package 'tree'

In this first example the package resource is used, and the name given is tree. And the version is set to 1.7.0-3. And the action taken is to install.

In this second example, you can see that there are only two of the four components of a resource. Since the default action for the package resource is to install whatever is named, this will install the latest version of the “tree” package.

So if you don’t need to specify any attributes or actions, because the defaults work well, you can skip the “do” and “end” parts of the block.

Let’s look at a couple more examples to make sure you’re comfortable with the resource syntax.

cron 'cookbooks_report' do
action node.tags.include?('cookbooks-report') ? :create : :delete
minute '0'
hour '0'
weekday '1'
user 'getchef'
mailto ''
home '/srv/supermarket/shared/system'
command %W{
cd /srv/supermarket/current &&
env RUBYLIB="/srv/supermarket/current/lib"
RAILS_ASSET_ID=`git rev-parse HEAD` RAILS_ENV="#{rails_env}"
bundle exec rake cookbooks_report
}.join(' ')
This example looks a bit more complicated, because of the number of attributes; however it follows the same pattern as any other resource. What’s happening in this example is that the cron resource is being used to create an entry in the crontab file. The action is going to conditionally be set to either “create” or “delete”.
If you’re not a Ruby developer, you may noticed that the “create” and “delete” looks a bit odd. They’re not wrapped in quotes like the values set in the attributes. And that’s because these are called symbols. A symbol is an immutable value, that you can think of as a sort of constant.

windows_service 'BITS' do
action :configure_startup
startup_type :manual

Here’s another example, that uses the Windows service resource to set the BITS service to have a startup type of manual. Again, it follows the same pattern as other resources. Here there are two symbols being used. If you’re not sure what attributes and actions are available, use the documentation and it’ll show you the available attributes and actions as well as what the defaults are set to.

If you look here at the documentation for the Windows Service resource you can see the data type for each property, as well as the available actions.

Okay, by now hopefully you have a pretty good grasp on how resources are specified. However resources aren’t the only thing that you’ll specify in a recipe. Because the Recipe DSL is based on Ruby, you can use whatever Ruby code you need to improve your recipes. Let’s check out some more of the Ruby based parts of the DSL.

First, “if” and “case” statements are usable and even commonplace inside of recipes. I’m going to assume you’re familiar with both, if not from Ruby, then from another programming language.

Both of these are plain old Ruby statements used for flow control. Here’s an example of an if statement to check which platform this recipe is being executed on.
if node['platform'] == 'ubuntu'
# do ubuntu things

And here’s a case statement doing roughly the same things as the if statement.
case node['platform']
when 'debian', 'ubuntu'
# do debian/ubuntu things
when 'redhat', 'centos', 'fedora'
# do redhat/centos/fedora things

These will both come in handy, especially for conditionally swapping out the name of a package to install. For example on Debian based platforms the Apache Web Server package is named apache2, and on RedHat based systems it’s named httpd.

So by conditionally setting a variable to the correct name, you can make your code cross distribution friendly.
Let’s look at some of the methods provided by the DSL to make writing recipes a bit easier.
In the if and case statements that I just showed, they both used the node hash to lookup the platform property. However the DSL has a couple of methods to make it easier, and those are the “platform” and “platform_family” methods.

Here’s an example that comes from Chef’s documentation, and they’ve taken it from the MySQL cookbook.
# the following code sample comes from the ``client`` recipe
# in the following cookbook:

if platform?('windows')
ruby_block 'copy libmysql.dll into ruby path' do
block do
require 'fileutils'
FileUtils.cp "#{node['mysql']['client']['lib_dir']}\\libmysql.dll",
not_if { File.exist?("#{node['mysql']['client']['ruby_dir']}\\libmysql.dll") }
In this example, the code is performing a check to determine if the platform is Windows based, and if so it will copy a dll file to the node, if it doesn’t already exist.

The “platform” method accepts multiple arguments, so you could pass in as many platforms as needed. Here’s the list of acceptable arguments on the screen. And I won’t bore you by reading it off, however you can see that it allows you to determine the OS type and distribution type.

The “platform_family” method is similar, however it groups distributions that are a part of the same family. For example, Ubuntu is based on Debian, so you could specify anything in the Debian family, and if the OS was Ubuntu, it would qualify.

Since conditionally setting a variable based on the platform is a common task, Chef also provides methods called “value_for_platform” and “value_for_platform_family” which allow you to more easily set platform specific values.
package_name = value_for_platform(
['centos', 'redhat', 'suse', 'fedora' ] => {
'default' => 'httpd'
['ubuntu', 'debian'] => {
'default' => 'apache2'

Here’s an example based on the name of the Apache package. In this example if the platform of the node executing this code is either: CentOS, RedHat, SuSE or Fedora, then the package_name variable will be set to “httpd.” If the platform is either Ubuntu or Debian, then it will be set to apache2. You’re not limited to returning a string, you could return any Ruby value you want. It’s not uncommon to return a hash containing multiple values for the given platform.

The “value_for_platform_family” method is similar, except it uses the platform family, just like the platform_family method I talked about previously.

package = value_for_platform_family(
['rhel', 'fedora', 'suse'] => 'httpd-devel',
'debian' => 'apache2-dev'

While there are several more methods provided in the DSL, there are just a few more that I’ll cover here. And the next one is the “include_recipe” method. As the name suggests, this allows you to include an existing recipe into your recipe. This basically will take whichever recipe you specify and inject it into yours, wherever the “include_recipe” method is called. This is a useful way to make sure that if your recipe requires another recipe to have been executed that it happens. And if you or someone else on your team accidentally calls the include_recipe method more than once for the same recipe, it’s only going to be executed the first time the method is called.

The next method to cover is the “attribute?” method. Recall that nodes have attributes, which are values that describe something about the state of a node. For example the IP address, MAC address and hostname are all attributes that are automatically populated by Ohai.
So if there’s an attribute that you’re resource relies on, you can use the “attribute?” method to make sure it has a value set.
if node.attribute?('ipaddress')
# the node has an ipaddress
In this example, if the node has an IP address attribute set then the code inside the if statement will execute.
This will come in handy for being able to skip code if an attribute is missing, or maybe set a useful default value.

The next two methods are the “data_bag” and “data_bag_item” methods. I mentioned the data bag briefly earlier. Data bags are basically global variables that store JSON objects. And you can use them to store any data that your nodes might need access to. This could be a list of usernames, access tokens, connection strings, or any other configuration data you’ll want to potentially share with nodes. By the way, the reason you can store things such as access tokens and connection strings is that the data bag supports encryption. If it didn’t, I wouldn’t recommend storing sensitive data inside.

data_bag('users').each do |user|
data_bag_item('users', user)
Being able to pull data from a secure location, and use those values to configure nodes is very useful, as you can image.

The final method that I want to show is the “shell_out” method. Now, ideally you’ll use a resource to configure a node. However, it’s possible that you’ll come across a task that doesn’t have a resource. And for that you can use the “shell_out” method. If you need to run a shell command, then you’ll want to use the shell_out method in place of using Ruby specific methods such as system, backticks, Process.spawn, etc, because shell out will work well cross platform and knows how to handle IO and STDERR.

Okay, there are other methods that are part of the DSL, and you can check them out in Chef’s documentation. The reason I’m not including them in the course is that I don’t want to overwhelm you with them all, especially since you probably won’t need more than a few to start.

Let’s wrap up this lesson here, and summarize the key things to remember.
First, resources represent some aspect of a node, and the desired state of that aspect. Resources all use the same pattern to implement them, starting with the resource type, the name, and then optionally any actions and attributes.

And in addition to resources, you can use plain old Ruby in your recipes. Which means anything you can do in Ruby, you can do in a recipe. And the DSL offers several methods that will help to write better recipes.

Alright, it’s time to test your memory and see how much you remember.

If you wanted to include an existing recipe in your recipe which method would you use?
If you answered include_recipe then you’re correct. The include_recipe method allows you to import another recipe in yours, allowing you to ensure that included recipe is run.

The next question is: How many components are there to a resources, and what are they?
The answer is that there are four components to a resource. The resource type, the name, actions, and attributes. And you may recall that actions will always have a default, and attributes may or may not have a default.

The next question is: What is the “shell_out” method used for, and what potential drawbacks to using it can use foresee?
The first part of the answer is that the “shell_out” method allows you to run shell commands in a cross platform way.
And the second part of the question is subjective, and therefore open ended. However the answer I’d give, is that by using the “shell_out” method to accomplish something that a resource already exists to handle, you could lose out on portability because while the shell_out method can run on multiple platforms, the command you run may not.

Okay, I think you’ve heard enough about how Chef works conceptually. It’s time to actually start in using it. So the rest of this course will be dedicated to actually using Chef. And before I can show you anything, I first need to install it. So that’s what I’ll cover in the next lesson. If you’ll ready to switch from learning to doing, then let’s get started in the next lesson!

About the Author
Learning Paths

Ben Lambert is a software engineer and was previously the lead author for DevOps and Microsoft Azure training content at Cloud Academy. His courses and learning paths covered Cloud Ecosystem technologies such as DC/OS, configuration management tools, and containers. As a software engineer, Ben’s experience includes building highly available web and mobile apps. When he’s not building software, he’s hiking, camping, or creating video games.

Covered Topics