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)
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?"
Interpolation
When strings have quotes within quotes, use double quotes (" "
) on the outer quotes, and then single quotes (' '
) for the inner quotes. For example:
Chef::Log.info("Loaded from aws[#{aws['id']}]")
"node['mysql']['secretpath']"
"#{ENV['HOME']}/chef.txt"
antarctica_hint = hint?('antarctica') if antarctica_hint['snow'] "There are #{antarctica_hint['penguins']} penguins here." else 'There is no snow here, and penguins like snow.' end
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
When %w
syntax uses a variable, such as |foo|
, double quoted strings should be used.
Right:
%w{openssl.cnf pkitool vars Rakefile}.each do |foo| template "/etc/openvpn/easy-rsa/#{foo}" do source "#{foo}.erb" ... end end
Wrong:
%w{openssl.cnf pkitool vars Rakefile}.each do |foo| template '/etc/openvpn/easy-rsa/#{foo}' do source '#{foo}.erb' ... end end
Example
WiX includes serveral 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 InSpec file
audit resource to verify if these three tools are present:
%w( candle.exe heat.exe light.exe ).each do |utility| describe file("C:/wix/#{utility}") do it { should be_file } end 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 <[email protected]>"
, 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. The chef-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 CHANGELOG.md with any changes, with the JIRA ticket and a brief description.
Cookbook Patterns
Good cookbook examples:
- https://github.com/chef-cookbooks/yum
- https://github.com/chef-cookbooks/mysql
- https://github.com/chef-cookbooks/httpd
- https://github.com/chef-cookbooks/php
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']
.
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' variables( :foo => 'bar' ) notifies :reload, 'service[whatever]' action :create end
File Modes
Always specify the file mode with a quoted 3-5 character string that defines the octal mode:
mode '755'
mode '0755'
mode 00755
Specify Resource Action?
A resource declaration does not require the action to be specified because the chef-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 end
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.
Whitespace Arrays
When %w
syntax uses a variable, such as |foo|
, double quoted strings should be used.
Right:
%w{openssl.cnf pkitool vars Rakefile}.each do |foo| template "/etc/openvpn/easy-rsa/#{foo}" do source "#{foo}.erb" ... end end
Wrong:
%w{openssl.cnf pkitool vars Rakefile}.each do |foo| template '/etc/openvpn/easy-rsa/#{foo}' do source '#{foo}.erb' ... end end
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.
Starting with chef-client version 12.0 you can use the shell_out
, shell_out!
and shell_out_with_system_locale
Recipe DSL methods to interface directly with mixlib-shellout
.
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 innode['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 usenode.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.
More about Ruby
To learn more about Ruby, see the following:
- http://www.ruby-lang.org/en/documentation/
- http://blog.loftninjas.org/2011/02/16/the-power-of-chef-and-ruby/
- http://www.codecademy.com/tracks/ruby
- http://www.ruby-doc.org/stdlib/
© Chef Software, Inc.
Licensed under the Creative Commons Attribution 3.0 Unported License.
The Chef™ Mark and Chef Logo are either registered trademarks/service marks or trademarks/servicemarks of Chef, in the United States and other countries and are used with Chef Inc's permission.
We are not affiliated with, endorsed or sponsored by Chef Inc.
https://docs-archive.chef.io/release/12-13/ruby.html