Ruby Guide

[edit on GitHub]

Ruby is a simple programming language:

  • Chef uses Ruby as its reference language to define the patterns that are found in resources, recipes, and cookbooks
  • Use these patterns to configure, deploy, and manage nodes across the network

Ruby is also a powerful and complete programming language:

  • Use the Ruby programming language to make decisions about what should happen to specific resources and recipes
  • Extend Chef in any manner that your organization requires

To learn more about Ruby, see:

As of Chef Client 14.0, Chef ships with Ruby 2.5.

Ruby Basics

This section covers the basics of Ruby.

Verify Syntax

Many people who are new to Ruby often find that it doesn’t take very long to get up to speed with the basics. For example, it’s useful to know how to check the syntax of a Ruby file, such as the contents of a cookbook named my_cookbook.rb:

$ ruby -c my_cookbook_file.rb

to return:

Syntax OK


Use a comment to explain code that exists in a cookbook or recipe. Anything after a # is a comment.

# This is a comment.

Local Variables

Assign a local variable:

x = 1


Do some basic arithmetic:

1 + 2           # => 3
2 * 7           # => 14
5 / 2           # => 2   (because both arguments are whole numbers)
5 / 2.0         # => 2.5 (because one of the numbers had a decimal place)
1 + (2 * 3)     # => 7   (you can use parentheses to group expressions)


Work with strings:

'single quoted'   # => "single quoted"
"double quoted"   # => "double quoted"
'It\'s alive!'    # => "It's alive!" (the \ is an escape character)
'1 + 2 = 5'       # => "1 + 2 = 5" (numbers surrounded by quotes behave like strings)

Convert a string to uppercase or lowercase. For example, a hostname named “Foo”:

node['hostname'].downcase    # => "foo"
node['hostname'].upcase      # => "FOO"

Ruby in Strings

Embed Ruby in a string:

x = 'Bob'
"Hi, #{x}"      # => "Hi, Bob"
'Hello, #{x}'   # => "Hello, \#{x}" Notice that single quotes don't work with #{}

Escape Character

Use the backslash character (\) as an escape character when quotes must appear within strings. However, you do not need to escape single quotes inside double quotes. For example:

'It\'s alive!'                        # => "It's alive!"
"Won\'t you read Grant\'s book?"      # => "Won't you read Grant's book?"


When strings have quotes within quotes, use double quotes (" ") on the outer quotes, and then single quotes (' ') for the inner quotes. For example:"Loaded from aws[#{aws['id']}]")
antarctica_hint = hint?('antarctica')
if antarctica_hint['snow']
  "There are #{antarctica_hint['penguins']} penguins here."
  'There is no snow here, and penguins like snow.'


Work with basic truths:

true            # => true
false           # => false
nil             # => nil
0               # => true ( the only false values in Ruby are false
                #    and nil; in other words: if it exists in Ruby,
                #    even if it exists as zero, then it is true.)
1 == 1          # => true ( == tests for equality )
1 == true       # => false ( == tests for equality )


Work with basic untruths (! means not!):

!true           # => false
!false          # => true
!nil            # => true
1 != 2          # => true (1 is not equal to 2)
1 != 1          # => false (1 is not equal to itself)

Convert Truths

Convert something to either true or false (!! means not not!!):

!!true          # => true
!!false         # => false
!!nil           # => false (when pressed, nil is false)
!!0             # => true (zero is NOT false).


Create lists using arrays:

x = ['a', 'b', 'c']   # => ["a", "b", "c"]
x[0]                  # => "a" (zero is the first index)
x.first               # => "a" (see?)
x.last                # => "c"
x + ['d']             # => ["a", "b", "c", "d"]
x                     # => ["a", "b", "c"] ( x is unchanged)
x = x + ['d']         # => ["a", "b", "c", "d"]
x                     # => ["a", "b", "c", "d"]

Whitespace Arrays

The %w syntax is a Ruby shortcut for creating an array without requiring quotes and commas around the elements.

For example:

if %w(debian ubuntu).include?(node['platform'])
  # do debian/ubuntu things with the Ruby array %w() shortcut

When %w syntax uses a variable, such as |foo|, double quoted strings should be used.


%w(openssl.cnf pkitool vars Rakefile).each do |foo|
  template "/etc/openvpn/easy-rsa/#{foo}" do
    source "#{foo}.erb"


%w(openssl.cnf pkitool vars Rakefile).each do |foo|
  template '/etc/openvpn/easy-rsa/#{foo}' do
    source '#{foo}.erb'


WiX includes several tools – such as candle (preprocesses and compiles source files into object files), light (links and binds object files to an installer database), and heat (harvests files from various input formats). The following example uses a whitespace array and the Chef InSpec file audit resource to verify if these three tools are present:

).each do |utility|
  describe file("C:/wix/#{utility}") do
    it { should be_file }


A Hash is a list with keys and values. Sometimes hashes don’t have a set order:

h = {
  'first_name' => 'Bob',
  'last_name'  => 'Jones'

And sometimes they do. For example, first name then last name:

h.keys              # => ["first_name", "last_name"]
h['first_name']     # => "Bob"
h['last_name']      # => "Jones"
h['age'] = 23
h.keys              # => ["first_name", "age", "last_name"]
h.values            # => ["Jones", "Bob", 23]

Regular Expressions

Use Perl-style regular expressions:

'I believe'  =~ /I/                       # => 0 (matches at the first character)
'I believe'  =~ /lie/                     # => 4 (matches at the 5th character)
'I am human' =~ /bacon/                   # => nil (no match - bacon comes from pigs)
'I am human' !~ /bacon/                   # => true (correct, no bacon here)
/give me a ([0-9]+)/ =~ 'give me a 7'     # => 0 (matched)


Use conditions! For example, an if statement

if false
  # this won't happen
elsif nil
  # this won't either
  # code here will run though

or a case statement:

x = 'dog'
case x
when 'fish'
 # this won't happen
when 'dog', 'cat', 'monkey'
  # this will run
  # the else is an optional catch-all


An if statement can be used to specify part of a recipe to be used when certain conditions are met. else and elseif statements can be used to handle situations where either the initial condition is not met or when there are other possible conditions that can be met. Since this behavior is 100% Ruby, do this in a recipe the same way here as anywhere else.

For example, using an if statement with the platform node attribute:

if node['platform'] == 'ubuntu'
  # do ubuntu things


A case statement can be used to handle a situation where there are a lot of conditions. Use the when statement for each condition, as many as are required.

For example, using a case statement with the platform node attribute:

case node['platform']
when 'debian', 'ubuntu'
  # do debian/ubuntu things
when 'redhat', 'centos', 'fedora'
  # do redhat/centos/fedora things

For example, using a case statement with the platform_family node attribute:

case node['platform_family']
when 'debian'
  # do things on debian-ish platforms (debian, ubuntu, linuxmint)
when 'rhel'
  # do things on RHEL platforms (redhat, centos, scientific, etc)

Call a Method

Call a method on something with .method_name():

x = 'My String'
x.split(' ')            # => ["My", "String"]
x.split(' ').join(', ') # => "My, String"

Define a Method

Define a method (or a function, if you like):

def do_something_useless( first_argument, second_argument)
  puts "You gave me #{first_argument} and #{second_argument}"

do_something_useless( 'apple', 'banana')
# => "You gave me apple and banana"
do_something_useless 1, 2
# => "You gave me 1 and 2"
# see how the parentheses are optional if there's no confusion about what to do

Ruby Class

Use the Ruby File class in a recipe. Because Chef has the file resource, use File to use the Ruby File class. For example:

execute 'apt-get-update' do
  command 'apt-get update'
  ignore_failure true
  not_if { File.exist?('/var/lib/apt/periodic/update-success-stamp') }

Include a Class

Use :include to include another Ruby class. For example:

::Chef::Recipe.send(:include, Opscode::OpenSSL::Password)

In non-Chef Ruby, the syntax is include (without the : prefix), but without the : prefix Chef Infra Client will try to find a provider named include. Using the : prefix tells Chef Infra Client to look for the specified class that follows.

Include a Parameter

The include? method can be used to ensure that a specific parameter is included before an action is taken. For example, using the include? method to find a specific parameter:

if %w(debian ubuntu).include?(node['platform'])
  # do debian/ubuntu things


if %w{rhel}.include?(node['platform_family'])
  # do RHEL things

Patterns to Follow

This section covers best practices for cookbook and recipe authoring.

git Etiquette

Although not strictly a Chef style thing, please always ensure your and are set properly in your .gitconfig file.

  • should be your given name (e.g., “Julian Dunn”)
  • should be an actual, working e-mail address

This will prevent commit log entries similar to "guestuser <login@Bobs-Macbook-Pro.local>", which are unhelpful.

Use of Hyphens

Cookbook and custom resource names should contain only alphanumeric characters. A hyphen (-) is a valid character and may be used in cookbook and custom resource names, but it is discouraged. Chef Infra Client will return an error if a hyphen is not converted to an underscore (_) when referencing from a recipe the name of a custom resource in which a hyphen is located.

Cookbook Naming

Use a short organizational prefix for application cookbooks that are part of your organization. For example, if your organization is named SecondMarket, use sm as a prefix: sm_postgresql or sm_httpd.

Cookbook Versioning

  • Use semantic versioning when numbering cookbooks.
  • Only upload stable cookbooks from master.
  • Only upload unstable cookbooks from the dev branch. Merge to master and bump the version when stable.
  • Always update with any changes, with the JIRA ticket and a brief description.


Name things uniformly for their system and component. For example:

  • attributes: node['foo']['bar']
  • recipe: foo::bar
  • role: foo-bar
  • directories: foo/bar (if specific to component), foo (if not). For example: /var/log/foo/bar.

Name attributes after the recipe in which they are primarily used. e.g. node['postgresql']['server'].

Parameter Order

Follow this order for information in each resource declaration:

  • Source
  • Cookbook
  • Resource ownership
  • Permissions
  • Notifications
  • Action

For example:

template '/tmp/foobar.txt' do
  source 'foobar.txt.erb'
  owner  'someuser'
  group  'somegroup'
  mode   '0644'
    foo: 'bar'
  notifies :reload, 'service[whatever]'
  action :create

File Modes

Always specify the file mode with a quoted 3-5 character string that defines the octal mode:

mode '755'
mode '0755'


mode 755

Specify Resource Action?

A resource declaration does not require the action to be specified because Chef Infra Client will apply the default action for a resource automatically if it’s not specified within the resource block. For example:

package 'monit'

will install the monit package because the :install action is the default action for the package resource.

However, if readability of code is desired, such as ensuring that a reader understands what the default action is for a custom resource or stating the action for a resource whose default may not be immediately obvious to the reader, specifying the default action is recommended:

ohai 'apache_modules' do
  action :reload

Symbols or Strings?

Prefer strings over symbols, because they’re easier to read and you don’t need to explain to non-Rubyists what a symbol is. Please retrofit old cookbooks as you come across them.


default['foo']['bar'] = 'baz'


default[:foo][:bar] = 'baz'

String Quoting

Use single-quoted strings in all situations where the string doesn’t need interpolation.

Whitespace Arrays

When %w syntax uses a variable, such as |foo|, double quoted strings should be used.


%w(openssl.cnf pkitool vars Rakefile).each do |foo|
  template "/etc/openvpn/easy-rsa/#{foo}" do
    source "#{foo}.erb"


%w(openssl.cnf pkitool vars Rakefile).each do |foo|
  template '/etc/openvpn/easy-rsa/#{foo}' do
    source '#{foo}.erb'

Shelling Out

Always use mixlib-shellout to shell out. Never use backticks, Process.spawn, popen4, or anything else!

The mixlib-shellout module provides a simplified interface to shelling out while still collecting both standard out and standard error and providing full control over environment, working directory, uid, gid, etc.

Constructs to Avoid

Avoid the following patterns:

  • node.normal - Avoid using attributes at normal precedence since they are set directly on the node object itself, rather than implied (computed) at runtime.
  • node.normal - Avoid using attributes at normal precedence since they are set directly on the node object itself, rather than implied (computed) at runtime.
  • if node.run_list.include?('foo') i.e. branching in recipes based on what’s in the node’s run-list. Better and more readable to use a feature flag and set its precedence appropriately.


A recipe should be clean and well-commented. For example:

# variables

connection_info = {
  host: '',
  port: '3306',
  username: 'root',
  password: 'm3y3sqlr00t'

# Mysql resources

mysql_service 'default' do
  port '3306'
  initial_root_password 'm3y3sqlr00t'
  action [:create, :start]

mysql_database 'wordpress_demo' do
  connection connection_info
  action :create

mysql_database_user 'wordpress_user' do
  connection connection_info
  database_name 'wordpress_demo'
  password 'w0rdpr3ssdem0'
  privileges [:create, :delete, :select, :update, :insert]
  action :grant

# Apache resources

httpd_service 'default' do
  listen_ports %w(80)
  mpm 'prefork'
  action [:create, :start]

httpd_module 'php' do
  notifies :restart, 'httpd_service[default]'
  action :create

# Php resources

package 'php-gd' do
  action :install

package 'php-mysql' do
  action :install

directory '/etc/php.d' do
  action :create

template '/etc/php.d/mysql.ini' do
  source 'mysql.ini.erb'
  action :create

httpd_config 'php' do
  source 'php.conf.erb'
  notifies :restart, 'httpd_service[default]'
  action :create

# wordpress resources

directory '/srv/wordpress_demo' do
  user 'apache'
  recursive true
  action :create

tar_extract '' do
  target_dir '/srv/wordpress_demo'
  tar_flags ['--strip-components 1']
  user 'apache'
  creates '/srv/wordpress_demo/index.php'
  action :extract

directory '/srv/wordpress_demo/wp-content' do
  user 'apache'
  action :create

httpd_config 'wordpress' do
  source 'wordpress.conf.erb'
    servername: 'wordpress',
    server_aliases: %w(,
    document_root: '/srv/wordpress_demo'
  notifies :restart, 'httpd_service[default]'
  action :create

template '/srv/wordpress_demo/wp-config.php' do
  source 'wp-config.php.erb'
  owner 'apache'
    db_name: 'wordpress_demo',
    db_user: 'wordpress_user',
    db_password: 'w0rdpr3ssdem0',
    db_host: '',
    db_prefix: 'wp_',
    db_charset: 'utf8',
    auth_key: 'You should probably use randomly',
    secure_auth_key: 'generated strings. These can be hard',
    logged_in_key: 'coded, pulled from encrypted databags,',
    nonce_key: 'or a ruby function that accessed an',
    auth_salt: 'arbitrary data source, such as a password',
    secure_auth_salt: 'vault. Node attributes could work',
    logged_in_salt: 'as well, but you take special care',
    nonce_salt: 'so they are not saved to your chef-server.',
    allow_multisite: 'false'
  action :create

Patterns to Avoid

This section covers things that should be avoided when authoring cookbooks and recipes.


Use node.default (or maybe node.override) instead of node.set because node.set is an alias for node.normal. Normal data is persisted on the node object. Therefore, using node.set will persist data in the node object. If the code that uses node.set is later removed, if that data has already been set on the node, it will remain.

Default and override attributes are cleared at the start of a Chef Infra Client run, and are then rebuilt as part of the run based on the code in the cookbooks and recipes at that time.

node.set (and node.normal) should only be used to do something like generate a password for a database on the first Chef Infra Client run, after which it’s remembered (instead of persisted). Even this case should be avoided, as using a data bag is the recommended way to store this type of data.

Cookbook Linting with Chef Workstation Tools

Chef Workstation includes Foodcritic for linting the Chef specific portion of your cookbook code, and Cookstyle for linting the Ruby specific portion of your code.

Foodcritic Linting

All cookbooks should pass Foodcritic rules before being uploaded.

$ foodcritic -P -f all your-cookbook

should return nothing.

Cookstyle Linting

All cookbooks should pass Cookstyle rules before being uploaded.

$ cookstyle your-cookbook

should return no offenses detected