Chef: overwrite templates in wrapper-cookbooks

02-04-2014 | Remy van Elst


Table of Contents


This article describes how to use a template in a wrapper-cookbook in Chef.

Background on Wrapper Cookbooks

The Chef Cookbook Wrapper Pattern is based upon a design convention where you customize an existing library cookbook by using a separate wrapper cookbook, which wraps the original cookbook with any related configuration changes.

A library cookbook is an existing cookbook, typically an open-source contribution from a user in the Chef community, designed for server configuration purposes.

A wrapper cookbook is a cookbook that wraps the original library cookbook with custom modifications or additions such as overriding a Chef attribute, changing a Chef template, converting a Chef attribute to a user-definable input, etc.

As the Chef community continues to grow, both in the number of active Chef developers and the range of available applications, finding an existing community cookbook that you want to leverage will become more of the norm than the exception. Therefore, it will be easier to find an existing cookbook that you can either use as-is or slightly modify for your own purposes. When possible, it's best to leverage an existing cookbook (assuming that it's actively being maintained) than trying to create your own custom cookbook from scratch or forking a cookbook repository.

Although forking a repository may initially seem like the easiest way to modify an existing cookbook, it will likely cause you more headaches over time as you try to maintain and upgrade your codebase over time. Therefore, it's recommended that you spend the extra time and effort to integrate a Wrapper Cookbook Pattern into your development routine because you will have a more manageable upgrade path for integrating future bug fixes and new functionality.

Source

Templates in wrapper-cookbooks

To override a template by just defining it again would result in it being written two times every Chef run, which is not what we want. Using this method, you can override the template from the default cookbook with a template in your wrapper-cookbook.

One of my clients uses graphite and wants to limit which users can login using LDAP in Apache. The graphite cookbook does not support this by default, but it works for all the other things.

Graphite itself has support for LDAP login, however, the client has experience with Apache LDAP and wants to use that so that other admins can manage it as well.

So what we want to do is overwrite the default apache template from the graphite cookbook with our template which has the LDAP data.

In the graphite cookbook we see the following piece of code which places the template for the graphite website in the apache sites-available folder:

template "#{node['apache']['dir']}/sites-available/graphite" do
  source "graphite-vhost.conf.erb"
  mode 0755
  variables(:timezone => node['graphite']['timezone'],
            :debug => node['graphite']['web']['debug'],
            :base_dir => node['graphite']['base_dir'],
            :doc_root => node['graphite']['doc_root'],
            :storage_dir => node['graphite']['storage_dir'],
            :cluster_servers => node['graphite']['web']['cluster_servers'],
            :carbonlink_hosts => node['graphite']['web']['carbonlink_hosts'],
            :memcached_hosts => node['graphite']['web']['memcached_hosts'],
     )
  notifies :reload, "service[apache2]", :immediately
end

We are going to override this template with extra variables for the LDAP connection in Apache.

Add a few node attributes, or place them in a data bag, whatever you like, for the LDAP:

{
    "tags": [],
    "graphite": {
        "ldap": {
            "password": "passw0rd",
            "server": "ldap.example.org",
            "enabled": true,
            "binddn": "uid=graphite,ou=Applications,dc=example,dc=org",
            "accessgroup": "cn=graphite_users,ou=Groups,dc=example,dc=org",
            "apachefilter": "uid?sub?(ObjectClass=*)",
            "userdn": "ou=Users,dc=example,dc=org",
            "port": 636
        }
    }
}

We are going to use these in the apache template.

Create a new cookbook:

knife cookbook create wrapper-graphite

Copy the template over from the graphite cookbook to the wrapper cookbook:

cp cookbooks/graphite/templates/default/graphite-vhost.conf.erb cookbooks/wrapper-graphite/templates/default/graphite-vhost.conf.erb

Add the LDAP settings to the template graphite-vhost.conf.erb in the wrapper-cookbook folder:

<Location />
  Order deny,allow
  Deny from All
  AuthName "Authorization Required"
  AuthType Basic
  # Needed for require-valid-user
  AuthzLDAPAuthoritative off
  AuthBasicProvider ldap
  AuthLDAPUrl "ldap://<%- @ldap_server %>/<%- @ldap_basedn %>?<%- @ldap_apachefilter %>"
  AuthLDAPBindDN "<%- @ldap_binddn %>"
  AuthLDAPBindPassword "<%- @ldap_password %>"
  Require ldap-group <%- @ldap_accessgroup %>
  Satisfy any
</Location>

Then edit your recipe, cookbooks/wrapper-graphite/recipes/default.rb. Add the basic header boilerplate and include the graphite recipe:

#
# Cookbook Name:: wrapper-graphite
# Recipe:: default
#
# Copyright 2014, EXAMPLE COMPANY
#
# License: GPLv3

include_recipe "graphite"

Don't forget to also add it to the metadata.rb file:

depends         "graphite"

Add the following magic to the wrapper cookbook. This is the part that overrides the template in the normal cookbook with the template from the wrapper cookbook:

begin
    r = resources(:template => "#{node['apache']['dir']}/sites-available/graphite")
    r.cookbook "wrapper-graphite"
    r.source "graphite-vhost.conf.erb"
    r.mode 0755
    r.variables(:timezone => node['graphite']['timezone'],
        :debug => node['graphite']['web']['debug'],
        :base_dir => node['graphite']['base_dir'],
        :doc_root => node['graphite']['doc_root'],
        :storage_dir => node['graphite']['storage_dir'],
        :cluster_servers => node['graphite']['web']['cluster_servers'],
        :carbonlink_hosts => node['graphite']['web']['carbonlink_hosts'],
        :memcached_hosts => node['graphite']['web']['memcached_hosts'],
        :ldap_enabled => node['graphite']['ldap']['enabled'],
        :ldap_server => node['graphite']['ldap']['server'],
        :ldap_port => node['graphite']['ldap']['port'],
        :ldap_binddn => node['graphite']['ldap']['binddn'],
        :ldap_basedn => node['graphite']['ldap']['basedn'],
        :ldap_accessgroup => node['graphite']['ldap']['access_group'],
        :ldap_password => node['graphite']['ldap']['password'],
        :ldap_apachefilter => node['graphite']['ldap']['apachefilter']
    )
    r.notifies :reload, "service[apache2]", :immediately
    rescue Chef::Exceptions::ResourceNotFound
        Chef::Log.warn "could not find template to override!"
end

This works because a chef-client run has multiple phases. The resource collection is the ordered list of resources, from the recipes in your expanded run list, that are to be run on a node.

During the resource collection phase we can manipulate attributes of the resources in the resource collection.

Because Chef uses a two-phase execution model (compile, then converge), you can manipulate the results of that compilation in many different ways before convergence happens.

As you can see we need to set all of the variables, even the ones that were already declared in the original cookbook. If you don't do this, it will bork.

Now by adding the wrapper-graphite recipe to a node instead of the graphite recipe, it will do all the things from the graphite recipe, except for the override we define here.

The big advantage of this approach for me is that I can update the upstream cookbook at any moment (for example, when a new graphite is releases or the upstream cookbook changes) while my own changes do not need to be backported in there.

Here is a blog entry from Opscode about wrapper cookbooks.


Tags: chef, deployment, devops, graphite, ldap, ruby,