Creating Your First Chef Cookbook

Updated by Linode Written by Elle Krout

Contribute on GitHub

Report an Issue | View File | Edit File

Cookbooks are one of the key components in Chef. They describe the desired state of your nodes, and allow Chef to push out the changes needed to achieve this state. Creating a cookbook can seem like an arduous task at first, given the sheer number of options provided and areas to configure, so in this guide we will walk through the creation of one of the first things people often learn to configure: A LAMP stack.

Creating Your First Chef Cookbook

Prior to using this guide, set up Chef with the Setting Up a Chef Server, Workstation, and Node guide. When following that guide, choose Ubuntu 16.04 as your Linux image for the Chef node. This is required because the MySQL Chef cookbook that will be used is not yet compatible with Ubuntu 18.04.

If needed, review the Beginner’s Guide to Chef.

The examples in this tutorial require a root user account. Readers who choose to use a limited user account will need to prefix commands with sudo where required when working on the Chef client node. If you have yet to create a limited user account, follow the steps in the Securing Your Server guide.

Create the Cookbook

  1. From your workstation, move to your cookbooks directory in chef-repo:

    cd chef-repo/cookbooks
    
  2. Create the cookbook. In this instance the cookbook is titled lamp_stack:

    chef generate cookbook lamp_stack
    
  3. Move to your cookbook’s newly-created directory:

    cd lamp_stack
    
  4. List the files located in the newly-created cookbook to see that a number of directories and files have been created:

    ls
    
      
    Berksfile  CHANGELOG.md  chefignore  LICENSE  metadata.rb  README.md  recipes  spec  test
    
    

    For more information about these directories see the Beginner’s Guide to Chef.

default.rb

The default.rb file in recipes contains the “default” recipe resources.

Because each section of the LAMP stack (Apache, MySQL, and PHP) will have its own recipe, the default.rb file is used to prepare your servers.

  1. From within your lamp_stack directory, navigate to the recipes folder:

    cd recipes
    
  2. Open default.rb and add the Ruby command below, which will run system updates:

    ~/chef-repo/cookbooks/lamp_stack/recipe/default.rb
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    #
    # Cookbook Name:: lamp_stack
    # Recipe:: default
    #
    #
    
    execute "update-upgrade" do
      command "sudo apt-get update && sudo apt-get upgrade -y"
      action :run
    end

    Recipes are comprised of a series of resources. In this case, the execute resource is used, which calls for a command to be executed once. The apt-get update && apt-get upgrade -y commands are defined in the command section, and the action is set to :run the commands.

    This is one of the simpler Chef recipes to write, and a good way to start out. Any other startup procedures that you deem important can be added to the file by mimicking the above code pattern.

  3. To test the recipe, add the LAMP stack cookbook to the Chef server:

    knife cookbook upload lamp_stack
    
  4. Test that the recipe has been added to the chef server:

    knife cookbook list
    
  5. Add the recipe to your chosen node’s run list, replacing nodename with your node’s name:

    knife node run_list add nodename "recipe[lamp_stack]"
    

    Because this is the default recipe, the recipe name does not need to be defined after lamp_stack cookbook in the code above.

  6. Access your chosen node and run the chef-client:

    chef-client
    

    It should output a successful Chef run. If not, review your code for any errors, usually defined in the output of the chef-client run.

Apache

Install and Enable

  1. In your Chef workstation, Create a new file under the ~/chef-repo/cookbooks/lamp_stack/recipes directory called apache.rb. This will contain all of your Apache configuration information.

  2. Open the file, and define the package resource to install Apache:

    ~/chef-repo/cookbooks/lamp_stack/apache.rb
    1
    2
    3
    
    package "apache2" do
      action :install
    end

    Again, this is a very basic recipe. The package resource calls to a package (apache2). This value must be a legitimate package name. The action is install because Apache is being installed in this step. There is no need for additional values to run the install.

  3. Set Apache to enable and start at reboot. In the same file, add the additional lines of code:

    ~/chef-repo/cookbooks/lamp_stack/apache.rb
    1
    2
    3
    
    service "apache2" do
      action [:enable, :start]
    end

    This uses the service resource, which calls on the Apache service. The enable action enables it upon startup, and start starts Apache.

    Save and close the apache.rb file.

  4. To test the Apache recipe, update the LAMP Stack recipe on the server:

    knife cookbook upload lamp_stack
    
  5. Add the recipe to a node’s run-list, replacing nodename with your chosen node’s name:

    knife node run_list add nodename "recipe[lamp_stack::apache]"
    

    Because this is not the default.rb recipe, the recipe name, apache, must be appended to the recipe value.

  6. From that node, run chef-client:

    chef-client
    

    If the recipe fails due to a syntax error, Chef will note it during the output.

  7. After a successful chef-client run, check to see if Apache is running:

    systemctl status apache2
    

    It should say that apache2 is running.

    Note
    Repeat Steps 5-7 to upload the cookbook and run chef-client as needed through the rest of this guide to ensure your recipes are working properly and contain no errors. Remember to replace the recipe name in the run list code when adding a new recipe.

Configure Virtual Hosts

This configuration is based off of the How to Install a LAMP Stack on Ubuntu 16.04 guide.

  1. Because multiple websites may need to be configured, use Chef’s attributes feature to define certain aspects of the virtual hosts file(s). The ChefDK has a built-in command to generate the attributes directory and default.rb file within a cookbook. Replace ~/chef-repo/cookbooks/lamp_stack with your cookbook’s path:

    chef generate attribute ~/chef-repo/cookbooks/lamp_stack default
    
  2. Within the new default.rb, create the default values of the cookbook:

    ~/chef-repo/cookbooks/lamp_stack/attributes/default.rb
    1
    
    default["lamp_stack"]["sites"]["example.com"] = { "port" => 80, "servername" => "example.com", "serveradmin" => "webmaster@example.com" }

    The prefix default defines that these are the normal values to be used in the lamp_stack where the site example.com will be called upon. This can be seen as a hierarchy: Under the cookbook itself are the site(s), which are then defined by their URL.

    The following values in the array (defined by curly brackets ({})) are the values that will be used to configure the virtual hosts file. Apache will be set to listen on port 80 and use the listed values for its server name, and administrator email.

    Should you have more than one available website or URL (for example, example.org), this syntax should be mimicked for the second URL:

    ~/chef-repo/cookbooks/lamp_stack/attributes/default.rb
    1
    2
    
    default["lamp_stack"]["sites"]["example.com"] = { "port" => 80, "servername" => "example.com", "serveradmin" => "webmaster@example.com" }
    default["lamp_stack"]["sites"]["example.org"] = { "port" => 80, "servername" => "example.org", "serveradmin" => "webmaster@example.org" }
  3. Return to your apache.rb file under recipes to call the attributes that were just defined. Do this with the node resource:

    ~/chef-repo/cookbooks/lamp_stack/recipes/apache.rb
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    #Install & enable Apache
    
    package "apache2" do
      action :install
    end
    
    service "apache2" do
      action [:enable, :start]
    end
    
    
    # Virtual Hosts Files
    
    node["lamp_stack"]["sites"].each do |sitename, data|
    end

    This calls in the values under ["lamp_stack"]["sites"]. Code added to this block will be generated for each value, which is defined by the word sitename. The data value calls the values that are listed in the array of each sitename attribute.

  4. Within the node resource, define a document root. This root will be used to define the public HTML files, and any log files that will be generated:

    ~/chef-repo/cookbooks/lamp_stack/apache.rb
    1
    2
    3
    
    node["lamp_stack"]["sites"].each do |sitename, data|
      document_root = "/var/www/html/#{sitename}"
    end
  5. However, this does not create the directory itself. To do so, the directory resource should be used, with a true recursive value so all directories leading up to the sitename will be created. A permissions value of 0755 allows for the file owner to have full access to the directory, while group and regular users will have read and execute privileges:

    ~/chef-repo/cookbooks/lamp_stack/apache.rb
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    node["lamp_stack"]["sites"].each do |sitename, data|
      document_root = "/var/www/html/#{sitename}"
    
      directory document_root do
        mode "0755"
        recursive true
      end
    
    end
  6. The template feature will be used to generate the needed virtual hosts files. Within the chef-repo directory run the chef generate template command with the path to your cookbook and template file name defined:

    chef generate template ~/chef-repo/cookbooks/lamp_stack virtualhosts
    
  7. Open and edit the virtualhosts.erb file. Instead of writing in the true values for each VirtualHost parameter, use Ruby variables. Ruby variables are identified by the <%= @variable_name %> syntax. The variable names you use will need to be defined in the recipe file:

    ~/chef-repo/cookbooks/lamp_stack/templates/virtualhosts.erb
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    <VirtualHost *:<%= @port %>>
            ServerAdmin <%= @serveradmin %>
            ServerName <%= @servername %>
            ServerAlias www.<%= @servername %>
            DocumentRoot <%= @document_root %>/public_html
            ErrorLog <%= @document_root %>/logs/error.log
            <Directory <%= @document_root %>/public_html>
                    Require all granted
            </Directory>
    </VirtualHost>

    Some variables should look familiar. They were created in Step 2, when naming default attributes.

  8. Return to the apache.rb recipe. In the space after the directory resource, use the template resource to call upon the template file just created:

    ~/chef-repo/cookbooks/lamp_stack/recipes/apache.rb
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    #Virtual Hosts Files
    
    node["lamp_stack"]["sites"].each do |sitename, data|
      document_root = "/var/www/html/#{sitename}"
    
      directory document_root do
        mode "0755"
        recursive true
      end
    
      template "/etc/apache2/sites-available/#{sitename}.conf" do
        source "virtualhosts.erb"
        mode "0644"
        variables(
          :document_root => document_root,
          :port => data["port"],
          :serveradmin => data["serveradmin"],
          :servername => data["servername"]
        )
      end
    
    end

    The name of the template resource should be the location where the virtual host file is placed on the nodes. The source is the name of the template file. Mode 0644 gives the file owner read and write privileges, and everyone else read privileges. The values defined in the variables section are taken from the attributes file, and they are the same values that are called upon in the template.

  9. The sites now need to be enabled in Apache, and the server restarted. This should only occur if there are changes to the virtual hosts, so the notifies value should be added to the template resource. notifies tells Chef when things have changed, and only then runs the commands:

    ~/chef-repo/cookbooks/lamp_stack/recipes/apache.rb
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    template "/etc/apache2/sites-available/#{sitename}.conf" do
      source "virtualhosts.erb"
      mode "0644"
      variables(
        :document_root => document_root,
        :port => data["port"],
        :serveradmin => data["serveradmin"],
        :servername => data["servername"]
      )
      notifies :restart, "service[apache2]"
    end

    The notifies command names the :action to be committed, then the resource, and resource name in square brackets.

  10. notifies can also call on execute commands, which will run a2ensiteand enable the sites we’ve made virtual hosts files for. Add the following execute command above the template resource code to create the a2ensite script:

    ~/chef-repo/cookbooks/lamp_stack/recipes/apache.rb
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    # [...]
    
    directory document_root do
      mode "0755"
      recursive true
    end
    
    execute "enable-sites" do
      command "a2ensite #{sitename}"
      action :nothing
    end
    
    template "/etc/apache2/sites-available/#{sitename}.conf" do
    
    # [...]

    The action :nothing directive means the resource will wait to be called on. Add a new notifies line above the previoues notifies line to the template resource code to use it:

    ~/chef-repo/cookbooks/lamp_stack/recipes/apache.rb
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    # [...]
    
    template "/etc/apache2/sites-available/#{sitename}.conf" do
      # [...]
      notifies :run, "execute[enable-sites]"
      notifies :restart, "service[apache2]"
    end
    
    # [...]
  11. The paths referenced in the virtual hosts files need to be created. Once more, this is done with the directory resource, and should be added before the final end tag:

    ~/chef-repo/cookbooks/lamp_stack/recipes/apache.rb
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    # [...]
    
    node["lamp_stack"]["sites"].each do |sitename, data|
      # [...]
    
      directory "/var/www/html/#{sitename}/public_html" do
        action :create
      end
    
      directory "/var/www/html/#{sitename}/logs" do
        action :create
      end
    end

Apache Configuration

With the virtual hosts files configured and your website enabled, configure Apache to efficiently run on your servers. Do this by enabling and configuring a multi-processing module (MPM), and editing apache2.conf.

The MPMs are all located in the mods_available directory of Apache. In this example the event MPM will be used, located at /etc/apache2/mods-available/mpm_event.conf. If we were planning on deploying to nodes of varying size we would create a template file to replace the original, which would allow for more customization of specific variables. In this instance, a cookbook file will be used to edit the file.

Cookbook files are static documents that are run against the document in the same locale on your servers. If any changes are made, the cookbook file makes a backup of the original file and replaces it with the new one.

  1. To create a cookbook file navigate to files/default from your cookbook’s main directory. If the directories do not already exist, create them:

    mkdir -p ~/chef-repo/cookbooks/lamp_stack/files/default/
    cd ~/chef-repo/cookbooks/lamp_stack/files/default/
    
  2. Create a file called mpm_event.conf and copy the MPM event configuration into it, changing any needed values:

    ~/chef-repo/cookbooks/lamp_stack/files/default/mpm_event.conf
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    <IfModule mpm_event_module>
            StartServers        2
            MinSpareThreads     6
            MaxSpareThreads     12
            ThreadLimit         64
            ThreadsPerChild     25
            MaxRequestWorkers   25
            MaxConnectionsPerChild  3000
    </IfModule>
  3. Return to apache.rb, and use the cookbook_file resource to call the file we just created. Because the MPM will need to be enabled, we’ll use the notifies command again, this time to execute a2enmod mpm_event. Add the execute and cookbook_file resources to the apache.rb file prior to the final end tag:

    ~/chef-repo/cookbooks/lamp_stack/recipes/apache.rb
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    # [...]
    
    node["lamp_stack"]["sites"].each do |sitename, data|
      # [...]
    
      execute "enable-event" do
        command "a2enmod mpm_event"
        action :nothing
      end
    
      cookbook_file "/etc/apache2/mods-available/mpm_event.conf" do
        source "mpm_event.conf"
        mode "0644"
        notifies :run, "execute[enable-event]"
      end
    end
  4. Within the apache2.conf the KeepAlive value should be set to off, which is the only change made within the file. This can be altered through templates or cookbook files, although in this instance a simple sed command will be used, paired with the execute resource. Update apache.rb with the new execute resource:

    ~/chef-repo/cookbooks/lamp_stack/recipes/apache.rb
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    # [...]
    
    directory "/var/www/html/#{sitename}/logs" do
      action :create
    end
    
    execute "keepalive" do
      command "sed -i 's/KeepAlive On/KeepAlive Off/g' /etc/apache2/apache2.conf"
      action :run
    end
    
    execute "enable-event" do
    
    # [...]

    Your apache.rb is now complete. An example of the final file is located here.

MySQL

Download the MySQL Library

  1. The Chef Supermarket has an OpsCode-maintained MySQL cookbook that sets up MySQL lightweight resources/providers (LWRPs) to be used. From the workstation, download and install the cookbook:

    knife cookbook site install mysql
    

    This will also install any and all dependencies required to use the cookbook. These dependencies include the smf and yum-mysql-community cookbooks, which in turn depend on the rbac and yum cookbooks.

  2. From the main directory of your LAMP stack cookbook, open the metadata.rb file and add a dependency to the MySQL cookbook:

    ~/chef-repo/cookbooks/lamp_stack/metadata.rb
    1
    
    depends          'mysql', '~> 8.5.1'
    Note
    Check the MySQL Cookbook’s Supermarket page to ensure this is the latest version of the cookbook. The MySQL Cookbook does not yet support Ubuntu 18.04.
  3. Upload these cookbooks to the server:

    knife cookbook upload mysql --include-dependencies
    

Create and Encrypt Your MySQL Password

Chef contains a feature known as data bags. Data bags store information, and can be encrypted to store passwords, and other sensitive data.

  1. On the workstation, generate a secret key:

    openssl rand -base64 512 > ~/chef-repo/.chef/encrypted_data_bag_secret
    
  2. Upload this key to your node’s /etc/chef directory, either manually by scp (an example can be found in the Setting Up Chef guide), or through the use of a recipe and cookbook file.

  3. On the workstation, create a mysql data bag that will contain the file rtpass.json for the root password:

    knife data bag create mysql rtpass.json --secret-file ~/chef-repo/.chef/encrypted_data_bag_secret
    
    Note
    Some knife commands require that information be edited as JSON data using a text editor. Your knife.rb file should contain a configuration for the text editor to use for such commands. If your knife.rb file does not already contain this configuration, add knife[:editor] = "/usr/bin/vim" to the bottom of the file to set vim as the default text editor.

    You will be asked to edit the rtpass.json file:

    ~/chef-repo/data_bags/mysql/rtpass.json
    1
    2
    3
    4
    
    {
      "id": "rtpass.json",
      "password": "password123"
    }

    Replace password123 with a secure password.

  4. Confirm that the rtpass.json file was created:

    knife data bag show mysql
    

    It should output rtpass.json. To ensure that is it encrypted, run:

    knife data bag show mysql rtpass.json
    

    The output will be unreadable due to encryption, and should resemble:

      
    WARNING: Encrypted data bag detected, but no secret provided for decoding.  Displaying encrypted data.
        id:       rtpass.json
        password:
          cipher:         aes-256-cbc
          encrypted_data: wpEAb7TGUqBmdB1TJA/5vyiAo2qaRSIF1dRAc+vkBhQ=
    
          iv:             E5TbF+9thH9amU3QmGxWmw==
    
          version:        1
        user:
          cipher:         aes-256-cbc
          encrypted_data: VLA00Wrnh9DrZqDcytvo0HQUG0oqI6+6BkQjHXp6c0c=
    
          iv:             6V+3ROpW9RG+/honbf/RUw==
    
          version:        1
    
    

Set Up MySQL

With the MySQL library downloaded and an encrypted root password prepared, you can now set up the recipe to download and configure MySQL.

  1. Open a new file in recipes called mysql.rb and define the data bag that will be used:

    ~/chef-repo/cookbooks/lamp_stack/recipes/mysql.rb
    1
    
    mysqlpass = data_bag_item("mysql", "rtpass.json")
  2. Thanks to the LWRPs provided through the MySQL cookbook, the initial installation and database creation for MySQL can be done in one resource:

    ~/chef-repo/cookbooks/lamp_stack/recipes/mysql.rb
    1
    2
    3
    4
    5
    6
    7
    
    mysqlpass = data_bag_item("mysql", "rtpass.json")
    
    mysql_service "mysqldefault" do
      version '5.7'
      initial_root_password mysqlpass["password"]
      action [:create, :start]
    end

    mysqldefault is the name of the MySQL service for this container. The inital_root_password calls to the value defined in the text above, while the action creates the database and starts the MySQL service.

    Note

    When running MySQL from your nodes you will need to define the socket:

    mysql -S /var/run/mysql-mysqldefault/mysqld.sock -p
    

PHP

  1. Under the recipes directory, create a new php.rb file. The commands below install PHP and all the required packages for working with Apache and MySQL:

    ~/chef-repo/cookbooks/lamp_stack/recipes/php.rb
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    package "php" do
      action :install
    end
    
    package "php-pear" do
      action :install
    end
    
    package "php-mysql" do
      action :install
    end
  2. For easy configuration, the php.ini file will be created and used as a cookbook file, much like the MPM module above. You can either:

    • Add the PHP recipe, run chef-client and copy the file from a node (located in /etc/php/7.0/cli/php.ini), or:
    • Copy it from this chef-php.ini sample. The file should be moved to the chef-repo/cookbooks/lamp_stack/files/default/ directory. This can also be turned into a template, if that better suits your configuration.
  3. php.ini is a large file. Search and edit the following values to best suit your Linodes. The values suggested below are for 2GB Linodes:

    ~/chef-repo/cookbooks/lamp_stack/files/default/php.ini
    1
    2
    3
    4
    5
    6
    7
    
    max_execution_time = 30
    memory_limit = 128M
    error_reporting = E_COMPILE_ERROR|E_RECOVERABLE_ERROR|E_ERROR|E_CORE_ERROR
    display_errors = Off
    log_errors = On
    error_log = /var/log/php/error.log
    max_input_time = 30
  4. Return to php.rb and append the cookbook_file resource to the end of the recipe:

    ~/chef-repo/cookbooks/lamp_stack/recipes/php.rb
    1
    2
    3
    4
    5
    
    cookbook_file "/etc/php/7.0/cli/php.ini" do
      source "php.ini"
      mode "0644"
      notifies :restart, "service[apache2]"
    end
  5. Because of the changes made to php.ini, a /var/log/php directory needs to be made and its ownership set to the Apache user. This is done through a notifies command and execute resource, as done previously. Append these resources to the end of php.rb:

    ~/chef-repo/cookbooks/lamp_stack/recipes/php.rb
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    execute "chownlog" do
      command "chown www-data /var/log/php"
      action :nothing
    end
    
    directory "/var/log/php" do
      action :create
      notifies :run, "execute[chownlog]"
    end

    The PHP recipe is now done! View an example of the php.rb file here.

  6. Ensure that your Chef server contains the updated cookbook, and that your node’s run list is up-to-date. Replace nodename with your Chef node’s name:

    knife cookbook upload lamp_stack
    knife node run_list add nodename "recipe[lamp_stack],recipe[lamp_stack::apache],recipe[lamp_stack::mysql],recipe[lamp_stack::php]"
    

You have just created a LAMP Stack cookbook. Through this guide, you should have learned to use the execute, package, service, node, directory, template, cookbook_file, and mysql_service resources within a recipe, as well as download and use LWRPs, create encrypted data bags, upload/update your cookbooks to the server, and use attributes, templates, and cookbook files, giving you a strong basis in Chef and cookbook creation for future projects.

More Information

You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.

Join our Community

Find answers, ask questions, and help others.

comments powered by Disqus

This guide is published under a CC BY-ND 4.0 license.