Nikolay Sturm's Blog

Musings about Development and Operations

Reducing Template Complexity in Chef Cookbooks

| Comments

At work we have a pretty heterogenous server setup, physical machines with public IPs, machines on private networks and then some more nodes in the cloud. When I setup Nagios monitoring for these servers, it lead to pretty complicated templates with nested conditionals because chef’s node object does carry lots of information, but it does not abstract away the specifics of the node. So, to get the public IP of an EC2 instance, you would use something like

node['cloud']['public_ipv4']

whereas on a physical machine, this would be

node['ipaddress']

To complicate matters, in certain situations, you might want to use the host’s local IP address, for instance when EC2 instances are communicating amongst themselves.

When I had to integrate a new feature, I figured it was time to get rid of all this incidental complexity and abstract away the details. So what I ended up with was simple wrapper classes in plain Ruby around node objects that would adhere to the same interface, tailored to the templates that would use the objects.

To give an example, let’s look at our Nagios hosts.cfg.erb file.

<% @nodes.each do |n| %>
    define host {
        use server
        <% if n.run_list.roles.include?('datacenter_x')
            address 1.2.3.4
            host_name <%= n['hostname'] %>-dcx
            alias <%= n['fqdn'] %>
            hostgroups dcx
        <% else %>
            address <%= n['ipaddress']
            <% if n['cloud'] %>
                host_name <%= n.name %>
                alias <%= n['cloud']['public_hostname'] %>
            <% else %>
                host_name <%= n['hostname'] %>
                alias <%= n['fqdn'] %>
            <% end %>
        <% end %>
        <% if n.run_list.roles.nil? || n.run_list.roles.empty? %>
            hostgroups all
        <% else %>
            hostgroups <%= n.run_list.roles.to_a.join(',') %>
        <% end %>
    }
<% end %>

Pretty complicated, isn’t it? After wrapping each node object, the template changes to this:

<% @nodes.each do |n| %>
    define host {
        use server
        address <%= n.nagios_ip %>
        host_name <%= n.hostname %>
        alias <%= n.alias %>
        hostgroups <%= n.hostgroups %>
    }
<% end %>

This is how templates should look like, no logic, no complexity.

In order to achieve this template simplicty, the logic has to move elsewhere. In this case I created different classes for our different kinds of nodes and wrapped them in the recipe:

nodes = nagios_nodes(search(:node, "*:*"))

With nagios_nodes() being defined in a library file in the cookbook.

def nagios_nodes(nodes)
  nodes.map do |node|
    if ...
      DatacenterNode.new(node)
    elsif ...
      CloudNode.new(node)
    else
      raise "Unexpected kind of node: #{node.name}"
    end
  end
end

The CloudNode could then look something like this:

class CloudNode
  def initialize(node)
    @node = node
  end

  def alias
    @node['cloud']['public_hostname']
  end

  ...
end

I am not following the Chef community much, so if you happen to know alternative approaches, please let me know in the comments.

Comments