Skip to content

Chef Guidelines

The purpose of this page should be to document how we at GitLab use chef, write cookbooks and configure nodes. It is a work in progress, however the points made here should be agreed upon by those who work with chef on a daily basis.

Keep a node simple - single purpose driven role applied on a node. For example, a front end web server should have a single role on it:

"run_list": [
"role[frontend-web-server]"
]

rather than:

"run_list": [
"role[base]",
"role[do-droplet]",
"role[frontend]",
"role[web-server]"
]

Similarly, avoid (manually) setting attributes on nodes directly. A node should be an instance of a role, making it a repetable and generic entity, rather than a patchwork of multiple different purposes. Cleaning this up would make the transition to cattle one step closer.

See cookbooks below.

Do we have to build a cookbook or is there a well maintained one in the Chef Supermarket we can use (e.g. via wrappers/lwrps)? The supermarket is excellent place to find cookbooks and should be the first place you look when thinking about creating a new cookbook. In many cases it is well worth spending time searching through these, rather than writing our own.

Including these cookbooks can be done in different ways by using so called wrapper cookbooks.

The Chef Cookbook Wrapper Pattern is based upon a design convention where you customize an existing “library” cookbook by using:

Cookbooks can gather common tasks into libraries to ensure that the code is DRY.

Example:

Only execute on supported platforms simplifies this by making it this

Cookbooks can use custom resources to abstract the access to groups of tasks (e.g. installing, configuring, starting…)

Example:

REDISIO cookbook with LWRPs

The redisio cookbook offers all the necessary LWRPs to install and configure a redis instance, but does not actually do anything itself if the default recipe is not called.

redisio_install "redis-installation" do
version '2.6.9'
download_url 'http://redis.googlecode.com/files/redis-2.6.9.tar.gz'
safe_install false
install_dir '/usr/local/'
end

By calling the LWRP we are agnostic to changes in the cookbook background, in the same way we access all chef resources. It does mean we need a wrapping recipe.

Cookbooks can rely on all configuration to be available via attributes on a node, allowing all configuration to be done via that route.

Example:

REDISIO cookbook with attributes and includes

run_list *%w[
recipe[redisio]
recipe[redisio::enable]
]
default_attributes({
'redisio' => {
'servers' => [
{'name' => 'master', 'port' => '6379', 'unixsocket' => '/tmp/redis.sock', 'unixsocketperm' => '755'},
]
}
})

By setting attributes on e.g. the role, we don’t have to make any programmatic changes to a cookbook since the logic is still in the default redisio recipe.

It is important to note that a wrapper cookbook does not extend functionality, only configures and includes the library cookbook!

Each chef run should ALWAYS describe the node in the same way. Avoid if statements which would cause chef resources to appear and disappear. Instead, make use of guards to skip over resources such as here. This ensures that we DECLARE what our infrastructure should look the same way every time chef-client runs.

Example of a guard in practice:

template '/etc/gitlab/gitlab.rb' do
mode '0600'
variables(gitlab_rb: gitlab_rb)
helper(:single_quote) { |value| value.nil? ? nil : "'#{value}'" }
notifies :run, 'execute[reload-gitlab]'
end
execute 'reload-gitlab' do
command "gitlab-ctl reconfigure"
action :nothing
not_if '${PROD?}'
end

This snippet would guard against gitlab-ctl reconfigure being called if the environment variable PROD is set.

Attributes are your friends:

  • all attributes used by a cookbook should be:
    • nested under a common, understandable hash e.g.

      sshd settings

      node['openssh']['sshd']['port']
      node['openssh']['sshd']['address_family']

      vs. unreadable settings:

      node['openssh_port']
      node['address_family']
    • Kept in the respective cookbook. Roles are the place where you can change settings, not in unrelated cookbooks. e.g. the haproxy cookbook is not the place for openssh settings.

    • Come with sane defaults. This plays into the testability of a cookbook. Never assume that attributes will be set elsewhere: either set defaults or fail.

  • Try to avoid setting attributes via cookbooks e.g. node.default['openssh']['sshd']['port'] = 23 this becomes hard to follow, when you are searching for the cause of issues.

The default README.md is NOT acceptable documentation.

A cookbook should not be considered DONE unless the following is documented:

  • Description - What does this cookbook do? (short and sweet)
  • Attributes - what are relevant and important attributes we expect, and what are their defaults?
  • Recipes - what does each recipe do?
  • LWRPs:
    • name
    • Actions
    • Attributes
    • Example

Chefspec is the fastest and easiest way to test cookbooks. Similar to RSpec, ChefSpec is a BDD testing framework which allows you to describe the expected behavior of each resource during a chef run. Chefspec tests MUST exist for a cookbook to be complete.

Kitchen tests are integration tests. They converge your cookbook and run it on an actual (e.g. vbox) node. This opens the door for lots of different tests, the first and foremost: does my recipe actually run. Furthermore you can write tests in essentially any test framework, e.g. BATS or serverspec.

knife (search|ssh|..) role:my-role ... returns only nodes for which my-role is specified in their run_list, not nested ones.

knife (search|ssh|..) roles:my-role ... returns all nodes which has my-role, directly and nestly specified.

Create or update file /etc/ipaddress.txt with desired IP address (or run curl ifconfig.co | sudo tee /etc/ipaddress.txt) and run chef-client.