Getting Started with Chef Solo. Part 2
WARNING: This article can be outdated. Better read my book about Chef: Cooking Infrastructure by Chef
Hello my dear friends. Today we will continue to talk about Chef Solo. You can find all example code here: github.com/le0pard/chef-solo-example/tree/2.0.
In the previous article we discussed how to use Chef Solo, learned about knife, librarian and vagrant tools, which help us to use and test Chef Solo kitchen. In this article we will learn cookbook structure and will write own cookbook.
Cookbook
A cookbook is a collection of Chef recipes. All cookbooks like a Chef are written in Ruby. You already have seen how we get nginx cookbook and use “source” recipe from it to install nginx on our server. Let’s look at the structure of this cookbook:
$ ls -la cookbooks/nginx
total 112
drwxr-xr-x 16 leo staff 544 Jan 4 19:24 .
drwxr-xr-x 6 leo staff 204 Jan 4 19:24 ..
drwxr-xr-x 15 leo staff 510 Jan 4 19:24 .git
-rw-r--r-- 1 leo staff 28 Jan 4 19:24 .gitignore
-rw-r--r-- 1 leo staff 3526 Jan 4 19:24 CHANGELOG.md
-rw-r--r-- 1 leo staff 10811 Jan 4 19:24 CONTRIBUTING.md
-rw-r--r-- 1 leo staff 37 Jan 4 19:24 Gemfile
-rw-r--r-- 1 leo staff 10850 Jan 4 19:24 LICENSE
-rw-r--r-- 1 leo staff 14633 Jan 4 19:24 README.md
drwxr-xr-x 8 leo staff 272 Jan 4 19:24 attributes
drwxr-xr-x 3 leo staff 102 Jan 4 19:24 definitions
drwxr-xr-x 3 leo staff 102 Jan 4 19:24 files
-rw-r--r--@ 1 leo staff 3283 Jan 4 19:24 metadata.rb
drwxr-xr-x 20 leo staff 680 Jan 4 19:24 recipes
drwxr-xr-x 5 leo staff 170 Jan 4 19:24 templates
drwxr-xr-x 3 leo staff 102 Jan 4 19:24 test
A cookbook can have:
- metadata.rb - a file, which contain all information about the cookbook (name, dependencies).
name "nginx"
maintainer "Opscode, Inc."
maintainer_email "cookbooks@opscode.com"
license "Apache 2.0"
description "Installs and configures nginx"
version "1.1.2"
recipe "nginx", "Installs nginx package and sets up configuration with Debian apache style with sites-enabled/sites-available"
recipe "nginx::source", "Installs nginx from source and sets up configuration with Debian apache style with sites-enabled/sites-available"
%w{ ubuntu debian centos redhat amazon scientific oracle fedora }.each do |os|
supports os
end
%w{ build-essential }.each do |cb|
depends cb
end
depends 'ohai', '>= 1.1.2'
%w{ runit bluepill yum }.each do |cb|
recommends cb
end
This is an important file, if you want to distribute your cookbook.
- attributes - a folder, which contain files with default attributes for recipes. In the nginx cookbook you can find such default attributes:
default['nginx']['version'] = "1.2.3"
default['nginx']['dir'] = "/etc/nginx"
default['nginx']['log_dir'] = "/var/log/nginx"
default['nginx']['binary'] = "/usr/sbin/nginx"
As you remember we can redefine all these attributes in the node file.
- definitions - a folder, which contain helpers from this cookbook. You can find this helper in the nginx cookbook:
define :nginx_site, :enable => true do
if params[:enable]
execute "nxensite #{params[:name]}" do
command "/usr/sbin/nxensite #{params[:name]}"
notifies :reload, "service[nginx]"
not_if do ::File.symlink?("#{node['nginx']['dir']}/sites-enabled/#{params[:name]}") end
end
else
execute "nxdissite #{params[:name]}" do
command "/usr/sbin/nxdissite #{params[:name]}"
notifies :reload, "service[nginx]"
only_if do ::File.symlink?("#{node['nginx']['dir']}/sites-enabled/#{params[:name]}") end
end
end
end
The helper “nginx_site” can enable/disable configuration from the folder “site-available” and reload nginx. I will show you how to use this helper.
- files - a folder, which contain files and these files just need to be copied on the server in the right place (it can be ssl keys, static configs, etc.)
- recipes - a folder, which contain all recipes from this cookbook. Each recipe is in a separate Ruby file:
$ ls -la cookbooks/nginx/recipes
total 152
drwxr-xr-x 20 leo staff 680 Jan 4 19:24 .
drwxr-xr-x 16 leo staff 544 Jan 4 19:24 ..
-rw-r--r-- 1 leo staff 1123 Jan 4 19:24 authorized_ips.rb
-rw-r--r-- 1 leo staff 792 Jan 4 19:24 commons.rb
-rw-r--r-- 1 leo staff 1114 Jan 4 19:24 commons_conf.rb
-rw-r--r-- 1 leo staff 1070 Jan 4 19:24 commons_dir.rb
-rw-r--r-- 1 leo staff 854 Jan 4 19:24 commons_script.rb
-rw-r--r-- 1 leo staff 1201 Jan 4 19:24 default.rb
-rw-r--r-- 1 leo staff 1551 Jan 4 19:24 http_echo_module.rb
-rw-r--r-- 1 leo staff 3412 Jan 4 19:24 http_geoip_module.rb
-rw-r--r-- 1 leo staff 814 Jan 4 19:24 http_gzip_static_module.rb
-rw-r--r-- 1 leo staff 1352 Jan 4 19:24 http_realip_module.rb
-rw-r--r-- 1 leo staff 797 Jan 4 19:24 http_ssl_module.rb
-rw-r--r-- 1 leo staff 1091 Jan 4 19:24 http_stub_status_module.rb
-rw-r--r-- 1 leo staff 738 Jan 4 19:24 ipv6.rb
-rw-r--r-- 1 leo staff 1704 Jan 4 19:24 naxsi_module.rb
-rw-r--r-- 1 leo staff 1059 Jan 4 19:24 ohai_plugin.rb
-rw-r--r-- 1 leo staff 2994 Jan 4 19:24 passenger.rb
-rw-r--r-- 1 leo staff 5218 Jan 4 19:24 source.rb
-rw-r--r-- 1 leo staff 1571 Jan 4 19:24 upload_progress_module.rb
As you remember we added this to the run_list:
"run_list": [
"recipe[nginx::source]"
]
This is run the source.rb recipe from the nginx cookbook. If you change it with by this:
"run_list": [
"recipe[nginx]"
]
This is run default recipe from nginx cookbook (file default.rb in recipes folder).
- templates - a folder, which contains Erb templates for this cookbook (these are nginx configs)
- test - a folder, which contain tests for this cookbook
First cookbook
Let’s create our first cookbook. Our custom cookbooks should be in the folder “site-cookbooks” (the folder “cookbooks” is used for vendor cookbooks and managed by librarian, so we will add this folder in gitignore). If you look in solo.rb, you can see such settings:
file_cache_path "/tmp/chef-solo"
cookbook_path [ "/tmp/chef-solo/site-cookbooks",
"/tmp/chef-solo/cookbooks" ]
This means what Chef will search needed cookbooks first in the site-cookbooks folder and nothing is found will try to search in the cookbooks folder. So if you create the nginx cookbook in site-cookbooks, Chef will try to use it first.
Let’s create a cookbook with named “tomatoes”:
$ mkdir site-cookbooks/tomatoes
$ mkdir site-cookbooks/tomatoes/recipes site-cookbooks/tomatoes/templates
$ mkdir site-cookbooks/tomatoes/templates/default
$ ls -la site-cookbooks/tomatoes
drwxr-xr-x 4 leo staff 136 Jan 5 14:50 .
drwxr-xr-x 4 leo staff 136 Jan 5 14:49 ..
drwxr-xr-x 2 leo staff 68 Jan 5 14:50 recipes
drwxr-xr-x 3 leo staff 102 Jan 5 14:50 templates
And create the file default.rb in the recipes folder with content:
package "git"
The command “package” is used to manage packages in the server. This command will install on server git package. For more info about this command you can read here: docs.opscode.com/chef/resources.html#package. Next, add our new recipe to the run list in our vagrant.json node:
"run_list": [
"recipe[nginx::source]",
"recipe[tomatoes]"
]
And test our kitchen again:
$ vagrant provision
[default] Running provisioner: Vagrant::Provisioners::ChefSolo...
[default] Generating chef JSON and uploading...
[default] Running chef-solo...
stdin: is not a tty
[Sat, 05 Jan 2013 13:07:34 +0000] INFO: *** Chef 0.10.10 ***
[Sat, 05 Jan 2013 13:07:34 +0000] INFO: Setting the run_list to ["recipe[nginx::source]", "recipe[tomatoes]"] from JSON
[Sat, 05 Jan 2013 13:07:34 +0000] INFO: Run List is [recipe[nginx::source], recipe[tomatoes]]
[Sat, 05 Jan 2013 13:07:34 +0000] INFO: Run List expands to [nginx::source, tomatoes]
[Sat, 05 Jan 2013 13:07:34 +0000] INFO: Starting Chef Run for precise64
[Sat, 05 Jan 2013 13:07:34 +0000] INFO: Running start handlers
[Sat, 05 Jan 2013 13:07:34 +0000] INFO: Start handlers complete.
...
[Sat, 05 Jan 2013 13:07:35 +0000] INFO: Processing package[git] action install (tomatoes::default line 1)
[Sat, 05 Jan 2013 13:07:50 +0000] INFO: package[git] installed version 1:1.7.9.5-1
[Sat, 05 Jan 2013 13:07:50 +0000] INFO: execute[nxensite default] sending reload action to service[nginx] (delayed)
[Sat, 05 Jan 2013 13:07:50 +0000] INFO: Processing service[nginx] action reload (nginx::source line 82)
[Sat, 05 Jan 2013 13:07:50 +0000] INFO: service[nginx] reloaded
[Sat, 05 Jan 2013 13:07:50 +0000] INFO: Chef Run complete in 16.410976 seconds
[Sat, 05 Jan 2013 13:07:50 +0000] INFO: Running report handlers
[Sat, 05 Jan 2013 13:07:50 +0000] INFO: Report handlers complete
As you can see the git package installed on the server. Let’s check this:
$ vagrant ssh
Welcome to Ubuntu 12.04.1 LTS (GNU/Linux 3.2.0-23-generic x86_64)
* Documentation: https://help.ubuntu.com/
Welcome to your Vagrant-built virtual machine.
Last login: Sat Jan 5 13:09:24 2013 from 10.0.2.2
vagrant@precise64:~$ git --version
git version 1.7.9.5
vagrant@precise64:~$ exit
logout
Connection to 127.0.0.1 closed.
All works fine.
Сonfigure nginx through our cookbook
Let’s configure nginx for our application. First of all add new attributes in the vagrant node (file “nodes/vagrant.json”):
{
"app": {
"name": "tomatoes",
"web_dir": "/var/data/www/apps/tomatoes"
},
"user":{
"name": "vagrant"
},
"nginx": {
"version": "1.2.3",
"default_site_enabled": true,
"source": {
"modules": ["http_gzip_static_module", "http_ssl_module"]
}
},
"run_list": [
"recipe[nginx::source]",
"recipe[tomatoes]"
]
}
Next, create a nginx template (“tomatoes/templates/default/nginx.conf.erb”):
server {
listen 80 default;
access_log <%= node.app.web_dir %>/logs/nginx_access.log;
error_log <%= node.app.web_dir %>/logs/nginx_error.log;
keepalive_timeout 10;
root <%= node.app.web_dir %>/public;
}
And create the index.html file in “tomatoes/files/default” (create this directory before) with content:
<h1>Hello from Chef Solo</h1>
This we will use to check what nginx will show after setup of settings.
Finally add this content to “tomatoes/recipes/default.rb”:
directory node.app.web_dir do
owner node.user.name
mode "0755"
recursive true
end
directory "#{node.app.web_dir}/public" do
owner node.user.name
mode "0755"
recursive true
end
directory "#{node.app.web_dir}/logs" do
owner node.user.name
mode "0755"
recursive true
end
template "#{node.nginx.dir}/sites-available/#{node.app.name}.conf" do
source "nginx.conf.erb"
mode "0644"
end
nginx_site "#{node.app.name}.conf"
cookbook_file "#{node.app.web_dir}/public/index.html" do
source "index.html"
mode "0755"
owner node.user.name
end
As you can see in the recipe node attributes available for us in the “node” variable. You can get this attributes in several ways:
node.app.web_dir
node['app']['web_dir']
node[:app][:web_dir]
This always will give you the same value from the app.web_dir attribute.
As you can see in the recipe code we created 3 directories, created a new config for nginx, enabled this config by “nginx_site” helper (this helper automatically reloads nginx) and put “index.html” into the server directory. After the launch command, “vagrant provision”, you should see this in your browser with url “http://localhost:8085/”:
Ruby Power!
As you can see in our recipe we created 3 directories by 3 commands. Better DRY this code. But how to do this? Simple! This is all Ruby code, so you can use it to make your recipe more powerful (and beautiful, of course):
package "git"
%w(public logs).each do |dir|
directory "#{node.app.web_dir}/#{dir}" do
owner node.user.name
mode "0755"
recursive true
end
end
template "#{node.nginx.dir}/sites-available/#{node.app.name}.conf" do
source "nginx.conf.erb"
mode "0644"
end
nginx_site "#{node.app.name}.conf"
cookbook_file "#{node.app.web_dir}/public/index.html" do
source "index.html"
mode 0755
owner node.user.name
end
We collect all subfolders in a Ruby array and create it in one cycle.
Summary
In the current article we have learned the Chef cookbook structure and how to write a simple Chef cookbook. In the next article we will look at the usage of roles in your Chef kitchen.
All example code you can find here: github.com/le0pard/chef-solo-example/tree/2.0.
That’s all folks! Thank you for reading till the end.