Chef Cookbooks
Chef is a powerful configuration management tool that automates system builds, configures software and ensures configurations are kept up-to-date. A recipe is the most basic type of configuration for Chef. A recipe installs, configures or starts a service, updates a configuration file or anything else that is required to configure a node. Cookbooks group multiple recipes in a collection. Chef executes these cookbooks on multiple nodes which could range from a few systems to thousands. Given the scale at which Chef operates, changes to the cookbooks should be rigorously tested prior to deploying them on live systems. This is where kitchen plays an important role. With kitchen, we can spin up a Docker container, test the cookbook changes in a container and then remove the container once the test is complete.
Components of a Cookbook
The main components of a cookbook are recipes, tests and spec directories. The format of a recipe takes is a resource declaration, name of the resource, and an action. Develop the Cookbook Test Cases section of this article covers test cases.
resource name do action :some_action end
Note: Anyone with shell scripting experience will naturally end the resource definition with done and not end. This will generate an unexpected end-of-input error.
FATAL: Stacktrace dumped to /root/.chef/local-mode-cache/cache/chef-stacktrace.out FATAL: Please provide the contents of the stacktrace.out file if you file a bug report ERROR: /root/cookbooks/kitchen_test/recipes/default.rb:14: syntax error, unexpected end-of-input, expecting keyword_end FATAL: Chef::Exceptions::ChildConvergeError: Chef run process exited unsuccessfully (exit code 1)
Creating a Cookbook
For our mongodb example cookbook, we will use two resources: package and service. The package resource will install or remove the software. The service resource will enable, start or disable a service from the system. You will notice on line 6 that we are using a Ruby list and combining two actions, enable and start.
Please note: this cookbook is an example only to showcase using kitchen. The Chef Supermarket has a cookbook available which provides much more functionality. You can download the cookbook from here.
The chef command below will create the cookbook structure. If chef isn’t installed on the system, you can download it as part of the ChefDK found here. Chef is deprecating the knife cookbook create command in favor of using the chef command.
root@ip-172-36-61-107:~/cookbooks# chef generate cookbook mongodb_server Generating cookbook mongodb_server - Ensuring correct cookbook file content - Committing cookbook files to git - Ensuring delivery configuration - Ensuring correct delivery build cookbook content - Adding delivery configuration to feature branch - Adding build cookbook to feature branch - Merging delivery content feature branch to master Your cookbook is ready. Type `cd mongodb_server` to enter it. There are several commands you can run to get started locally developing and testing your cookbook. Type `delivery local --help` to see a full list. Why not start by writing a test? Tests for the default recipe are stored at: test/recipes/default_test.rb If you'd prefer to dive right in, the default recipe can be found at: recipes/default.rb
The chef generate command creates the directory structure below. Note: the default_test.rb has some sample content, so be sure to replace this content as needed.
root@ubuntu-chef:~/cookbooks# tree mongodb_server mongodb_server ├── Berksfile ├── chefignore ├── LICENSE ├── metadata.rb ├── README.md ├── recipes │ └── default.rb ├── spec │ ├── spec_helper.rb │ └── unit │ └── recipes │ └── default_spec.rb └── test └── integration └── default └── default_test.rb
This is the cookbook we will use for the kitchen tests. Create the recipe file under mongodb_server/recipes and name it mongodb.rb. The “rb” is the extention for Ruby and is used for all recipes and kitchen tests.
package 'mongodb-server' do action :install end package 'mongodb-dev' do action :install end service 'mongodb' do action [ :enable, :start ] end
Update the default.rb file in the mongodb_server/recipe to include the mongodb.rb file.
include_recipe 'mongodb_server::mongodb'
Execute the chef-client command to run the recipe locally. Alternatively, if you have setup Hosted Chef, you may omit “–local-mode” from the command below.
root@ip-172-36-61-107:~/cookbooks# chef-client --local-mode -r "recipe[mongodb_server]" [0000-00-00T00:00:00+00:00] WARN: No config file found or specified on command line, using command line options. Starting Chef Client, version 12.13.37 resolving cookbooks for run list: ["mongodb_server"] Synchronizing Cookbooks: - mongodb_server (0.1.0) Installing Cookbook Gems: Compiling Cookbooks... Converging 3 resources Recipe: mongodb_server::mongodb * apt_package[mongodb-server] action install (up to date) * apt_package[mongodb-dev] action install (up to date) * service[mongodb] action enable (up to date) * service[mongodb] action start (up to date) Running handlers: Running handlers complete Chef Client finished, 0/4 resources updated in 01 seconds
Testing Chef Cookbooks with Kitchen
The Chef Development Kit (ChefDK) includes the kitchen command to validate changes to a cookbook . With the kitchen-docker driver, kitchen automatically creates Docker containers, applies the cookbook and then destroys the container. In order to install Docker, please follow the instructions here.
We have to initialize the environment using the command kitchen init. Since we are using the Docker driver, we pass the “-D docker” argument so Chef will use docker instead of the default vagrant. kitchen will automatically download and install the Docker gem package.
root@ubuntu-chef:~/cookbooks/mongodb_server# kitchen init -D docker create .kitchen.yml create chefignore Successfully installed docker-0.4.0 Parsing documentation for docker-0.4.0 Done installing documentation for docker after 0 seconds 1 gem installed
Configuring .kitchen.yml File
The init phase creates the configuration file named .kitchen.yml. You can leave the default .kitchen.yaml, or add or remove any other items required for testing with the cookbook. We removed the CentOS platform since we are only using Ubuntu in this example.
We are using the default verifier named Inspec. The suites heading, describes what we are wanting to test. We are only testing the default recipe, but in a real-world cookbook, we may define different recipes to support the cookbook. Underneath the suites -> verifier subheading lists the directory where the test cases reside. The test cases will not execute since Chef will check a different location.
--- driver: name: docker provisioner: name: chef_zero # You may wish to disable always updating cookbooks in CI or other testing environments. # For example: # always_update_cookbooks: <%= !ENV['CI'] %> always_update_cookbooks: true verifier: name: inspec platforms: - name: ubuntu-14.04 suites: - name: default run_list: - recipe[mongodb_server::default] verifier: inspec_tests: - test/recipes/default attributes:
After running kitchen init, you can see the list of instances available by running list. Below is only one instance since we removed Centos from the .kitchen.yml file.
root@ubuntu-chef:~/cookbooks/mongodb_server# kitchen list Instance Driver Provisioner Verifier Transport Last Action Last Error default-ubuntu-1404 Docker ChefSolo Busser Ssh <Not Created> <None>
Now create the virtual instance used for testing with the kitchen create command. Note: this command usually takes 3-5 minutes to complete.
root@ubuntu-chef:~/cookbooks/mongodb_server# kitchen create -----> Starting Kitchen (v1.20.0) -----> Creating <default-ubuntu-1404>... Sending build context to Docker daemon 201.2kB Step 1/17 : FROM ubuntu:14.04 14.04: Pulling from library/ubuntu 324d088ce065: Pulling fs layer 2ab951b6c615: Pulling fs layer 9b01635313e2: Pulling fs layer 04510b914a6c: Pulling fs layer 83ab617df7b4: Pulling fs layer 04510b914a6c: Waiting 83ab617df7b4: Waiting 9b01635313e2: Verifying Checksum 9b01635313e2: Download complete
We can use docker commands to see the images and containers kitchen created. If you need to check the container, use the command docker exec -it exec -it 83043dc787e8 /bin/bash
root@ubuntu-chef:~/cookbooks/mongodb_server# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b6948b229272 71cdf1224f9a "/usr/sbin/sshd -D -…" 2 minutes ago Up 2 minutes 0.0.0.0:32768->22/tcp defaultubuntu1604-chefuser-ubuntuchef-9fphugrq root@ubuntu-chef:~/cookbooks/mongodb_server# docker images REPOSITORY TAG IMAGE ID CREATED SIZE <none> <none> 71cdf1224f9a 2 minutes ago 216MB ubuntu 14.04 0b1edfbffd27 3 days ago 113MB
The next step is to install and run the cookbook in the virtual instances created from the step above using the converge sub-command. This command may take several minutes to complete.
root@ubuntu-chef:~/cookbooks/mongodb_server# kitchen converge -----> Starting Kitchen (v1.21.1) -----> Converging <default-ubuntu-1404>... Preparing files for transfer Preparing dna.json Preparing current project directory as a cookbook Removing non-cookbook files before transfer Preparing solo.rb -----> Installing Chef Omnibus (install only if missing) Downloading https://omnitruck.chef.io/install.sh to file /tmp/install.sh Trying wget... Download complete. ubuntu 16.04 x86_64 Getting information for chef stable for ubuntu...
Develop the Cookbook Test Cases
After running kitchen converge, we can run test cases using the verify sub-command. Below is an example test script which verifies the state of Mongo. The script checks that the default mongodb port is in a LISTEN state, the mongodb-server package is installed, a user is defined as mongodb, /etc/mongodb.conf exists and finally checks if the service is running.
describe port(27017) do it { should be_listening } end describe package('mongodb-server') do it { should be_installed} end describe user('mongodb') do it { should exist } end describe file('/etc/mongodb.conf') do it { should be_file } end describe service('mongodb') do it { should be_running } end
Use the verify argument to execute the test cases written in the previous step.
root@ubuntu-chef:~/cookbooks/mongodb_server# kitchen verify -----> Starting Kitchen (v1.20.0) -----> Verifying <default-ubuntu-1404>... Loaded tests from {:path=>".root.cookbooks.mongodb_server.test.recipes.default"} Profile: tests from {:path=>"/root/cookbooks/recipes/test/recipes/default"} (tests from {:path=>".root.cookbooks.mongodb_server.test.recipes.default"}) Version: (not specified) Target: ssh://kitchen@localhost:32769 Port 27017 ✔ should be listening System Package mongodb-server ✔ should be installed User mongodb ✔ should exist File /etc/mongodb.conf ✔ should be file Service mongodb ✔ should be running Test Summary: 5 successful, 0 failures, 0 skipped Finished verifying <default-ubuntu-1404> (0m0.24s). -----> Kitchen is finished. (0m1.50s)
The last step uses the destroy sub-command which removes the Docker container.
root@ubuntu-chef:~/cookbooks/mongodb_server# kitchen destroy -----> Starting Kitchen (v1.20.0) -----> Destroying <default-ubuntu-1404>... UID PID PPID C STIME TTY TIME CMD root 27070 19304 0 May03 ? 00:00:00 /usr/sbin/sshd -D -o UseDNS=no -o UsePAM=no -o PasswordAuthentication=yes -o UsePrivilegeSeparation=no -o PidFile=/tmp/sshd.pid usbmux 34586 27070 0 09:19 ? 00:00:18 /usr/bin/mongod --unixSocketPrefix=/var/run/mongodb --config /etc/mongodb.conf run 41f85bcf65e1707d152d2e8c351ed4289d4e65d355328acc1e4914457d5193d4 41f85bcf65e1707d152d2e8c351ed4289d4e65d355328acc1e4914457d5193d4 Finished destroying <default-ubuntu-1404> (0m1.88s). -----> Kitchen is finished. (0m2.93s)