Chef

Table Of Contents

Chef Style Guide

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

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

Comments

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

Math

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 parens to group expressions)

Strings

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)

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. For example:

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

Truths

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 )

Untruths

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 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).

Arrays

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
end

Hash

A Hash is a list with keys and values. Sometimes they 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)

Statements

Use conditions! For example, an if statement

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

or a case statement:

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

if

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
end

case

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
end

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)
end

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}"
end

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 parens 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
  only_if { apt_installed? }
  not_if { ::File.exist?('/var/lib/apt/periodic/update-success-stamp') }
end

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 the chef-client will try to find a provider named include. Using the : prefix tells the chef-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 ["debian", "ubuntu"].include?(node["platform"])
  # do debian/ubuntu things
end

or:

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

Log Entries

Chef::Log extends Mixlib::Log and will print log entries to the default logger that is configured for the machine on which the chef-client is running. (To create a log entry that is built into the resource collection, use the log resource instead of Chef::Log.)

The following log levels are supported:

Log Level Syntax
Debug Chef::Log.debug("string")
Error Chef::Log.error("string")
Fatal Chef::Log.fatal("string")
Info Chef::Log.info("string")
Warn Chef::Log.warn("string")

Note

The parentheses are optional, e.g. Chef::Log.info "string" `` may be used instead of ``Chef::Log.info("string").

The following examples show using Chef::Log entries in a recipe.

The following example shows a series of fatal Chef::Log entries:

unless node['splunk']['upgrade_enabled']
  Chef::Log.fatal('The chef-splunk::upgrade recipe was added to the node,')
  Chef::Log.fatal('but the attribute `node["splunk"]["upgrade_enabled"]` was not set.')
  Chef::Log.fatal('I am bailing here so this node does not upgrade.')
  raise
end

service 'splunk_stop' do
  service_name 'splunk'
  supports :status => true
  provider Chef::Provider::Service::Init
  action :stop
end

if node['splunk']['is_server']
  splunk_package = 'splunk'
  url_type = 'server'
else
  splunk_package = 'splunkforwarder'
  url_type = 'forwarder'
end

splunk_installer splunk_package do
  url node['splunk']['upgrade']["#{url_type}_url"]
end

if node['splunk']['accept_license']
  execute 'splunk-unattended-upgrade' do
    command "#{splunk_cmd} start --accept-license --answer-yes"
  end
else
  Chef::Log.fatal('You did not accept the license (set node["splunk"]["accept_license"] to true)')
  Chef::Log.fatal('Splunk is stopped and cannot be restarted until the license is accepted!')
  raise
end

The full recipe is the upgrade.rb recipe of the chef-splunk cookbook that is maintained by Chef.

The following example shows using multiple Chef::Log entry types:

...

begin
  aws = Chef::DataBagItem.load(:aws, :main)
  Chef::Log.info("Loaded AWS information from DataBagItem aws[#{aws['id']}]")
rescue
  Chef::Log.fatal("Could not find the 'main' item in the 'aws' data bag")
  raise
end

...

The full recipe is in the ebs_volume.rb recipe of the database cookbook that is maintained by Chef.

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 user.name and user.email are set properly in your .gitconfig file.

  • user.name should be your given name (e.g., “Julian Dunn”)
  • user.email 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.

Cookbook Naming

  • Avoid dashes in cookbook names. This is because lightweight resources use the cookbook name as part of the resource name, so the methods become very awkward. In particular, since - can’t be part of a symbol in Ruby, you won’t be able to use lightweight resources in any cookbook with - in its name.
  • All organization application cookbooks should be prefixed with a short organizational prefix, such as ‘sm’ for ‘SecondMarket’ (e.g. ‘smpostgresql’)

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 CHANGELOG.md with any changes, with the JIRA ticket and a brief description.

Naming

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'].

Default Recipe

Don’t use the default recipe (leave it blank). Instead, create recipes called server or client (or other).

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   00644
  variables(
    :foo => 'bar'
  )
  notifies :reload, 'service[whatever]'
  action :create
end

File Modes

Always specify all five digits of the file mode, and not as a string.

Right:

mode 00644

Wrong:

mode "644"

Always Specify Action

A resource declaration SHOULD specify the action to be taken.

Right:

package 'monit' do
  action :install
end

Wrong:

package 'monit'

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.

Right:

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

Wrong:

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

String Quoting

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

Shelling Out

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

As of chef-client version 12 you can use shell_out, shell_out! and shell_out_with_system_locale directly in Recipe DSL.

Constructs to Avoid

Avoid the following patterns:

  • node.set / normal_attributes - Avoid using attributes at normal precedence since they are set directly on the node object itself, rather than implied (computed) at runtime.
  • node.set_unless - Can lead to weird behavior if the node object had something set. Avoid unless altogether necessary (one example where it’s necessary is in node['postgresql']['server']['password'])
  • 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.
  • node['foo']['bar'] i.e. setting normal attributes without specifying precedence. This is deprecated in Chef 11, so either use node.set['foo']['bar'] to replace its precedence in-place or choose the precedence to suit.

Recipes

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

###########
# variables
###########

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

#################
# Mysql resources
#################

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

mysql_database 'wordpress_demo' do
  connection connection_info
  action :create
end

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

##################
# Apache resources
##################

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

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

###############
# Php resources
###############

# php_runtime 'default' do
#   action :install
# end

package 'php-gd' do
  action :install
end

package 'php-mysql' do
  action :install
end

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

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

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

#####################
# wordpress resources
#####################

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

tar_extract 'https://wordpress.org/wordpress-4.1.tar.gz' do
  target_dir '/srv/wordpress_demo'
  tar_flags ['--strip-components 1']
  user 'apache'
  creates '/srv/wordpress_demo/index.php'
  action :extract
end

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

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

template '/srv/wordpress_demo/wp-config.php' do
  source 'wp-config.php.erb'
  owner 'apache'
  variables(
    db_name: 'wordpress_demo',
    db_user: 'wordpress_user',
    db_password: 'w0rdpr3ssdem0',
    db_host: '127.0.0.1',
    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
end

Patterns to Avoid

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

node.set

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.

Normal and override attributes are cleared at the start of the chef-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-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.

Use the Chef DK

This section covers best practices for cookbook and recipe authoring.

Foodcritic Linting

All cookbooks should pass Foodcritic rules before being uploaded.

$ foodcritic -f all your-cookbook

should return nothing.