Templates, Attributes, Resources, and Dependency Management

This text is the third in a long series to correct an extremely disastrous talk at CakeFest 2011. It will also hopefully apply to more than CakePHP.

Templates

Templating in Chef is much like templating in any other framework. Chef templates use ERB - eRuby - which is a templating system that embeds Ruby into a document. If you know any Ruby, this should be a fairly trivial thing to learn. For CakePHP developers, think ctp files :)

For PHP developers, the big difference is that it uses short-syntax - <? ?> - and the question marks are replaced with percent signs - <% %>. Along with the short syntax, there isn’t an echo statement, so if you want to echo a variable, <%= var %> will do it.

Another key change is in variable output within strings. In PHP, we can do:

    $foo = 'bar';
    $baz = "{$foo}";
    echo $baz; // outputs: bar

In Ruby, we might do the following:

    foo = 'bar'
    baz = "#{foo}"
    puts baz # outputs: bar

That’s pretty neat I think, not that big of a change.

Templates alone aren’t very useful, but they are normally used in conjunction with the Template resource in Chef as follows:

template "/path/to/where/template/should/be/written/to" do
  source "some_template.erb"
  owner "root"
  group "root"
  mode 0644
  variables(
    :var_name   => "value"
    :an_array   => [ "of", "values" ]
  )
end

In the above example, we are creating the file /path/to/where/template/should/be/written/to from the some_template.erb file. We’re also setting the owner, group and the permissions of the file. The variables var_name and an_array would be available as variables in the view.

Pretty straight-forward I think.

Attributes

Attributes are fairly straight-forward. They allow you to set defaults for a particular cookbook by setting/modifying the variables in your DNA config.

# Deploy settings
default[:server][:production][:dir] = "/apps/production"
default[:server][:production][:hostname] = "example.com"
default[:server][:staging][:dir] = "/apps/staging"
default[:server][:staging][:hostname] = "example.net"
if node.nginx.attribute?("varnish")
  # ... do stuff on NGINX varnish stuff
end
override[:server][:extensions][:gzip] = true

In the above, we are setting the default paths and hostnames for the production and staging server environments, as well as setting some varnish-related configuration if node[:nginx][:varnish] exists; We also override the configured server extensions to ensure gzip will always be turned on, in case someone tries to turn it off via the DNA configuration.

I normally just set defaults and forget about it. For those deploying across multiple Operating Systems, we can do the following to set variables depending upon the OS:

# Platform specific settings
case platform
when "redhat","centos","fedora","suse"
  # Some settings here
when "debian","ubuntu"
  # Debian-related configuration
when "windows"
  # Windows-specific defaults
else
  # When all else fails...
end

Attributes are a good way of providing simple, cross-platform compatible defaults that can be later tweaked by way of DNA files. They allow you to provide a baseline configuration that might be modified in cases where we have more memory, lower disk throughput, or some other deviation from the norm.

If you find that you’re constantly setting the same settings in your DNA file, feel free to modify the defaults to keep your setups DRY.

Custom Resources

We’re going to fake out Custom Resources because the real way is a definite PITA. You can read about it in the Chef docs if you want.

Definitions are the poor smart mans way of creating resources. Definitions are cool because you can import them across projects, and are very simple to understand by all. I would create a definition when you have a large amount of code you are executing multiple times, maybe in a loop, that really only varies by two or three variables.

A good candidate is bringing up a new virtualhost for nginx.

# Definition
define :nginx_up, :enable => true do
  name = "#{node[:nginx][:dir]}/sites-available/#{params[:name]}"
  template name do
    source "html.erb"
    owner "deploy"
    group "deploy"
    mode 0644
    variables(params[:variables])
  end
  nginx_site params[:hostname] do
    action :enable
  end
end

The above definition creates a templated virtualhost file and then enables it using nginx. While it is simple, it only displays a small portion of what you can do with definitions. You could simply bring up a virtualhost, or automatically create EBS-mounted volumes with 2 partitions, each having certain folders. That’s merely up to your imagination. Personally, I like the beauty of typing the following into my cookbooks:

# info is an array of data
nginx_up "#{info[:hostname]}.#{info[:base]}" do
  hostname "#{info[:hostname]}.#{info[:base]}"
  variables(info[:variables])
end

Instead of the alternative. But thats just me.

Dependency Management

This one is simple. Sometimes you need to have another cookbook loaded for your current cookbook/recipe to work. For example, CakePHP applications depend upon php being installed, and thus they depend upon the php recipe.

We can specify that an entire cookbook depends upon another cookbook (or recipe) in the metadata file (json or ruby, I prefer ruby):

maintainer        "Jose Diaz-Gonzalez"
maintainer_email  "support@savant.be"
license           "MIT"
description       "Installs and maintains php and php modules"
depends           "nginx"
depends           "mysql::client"

If we conditionally need it for a particular recipe/definition, we just include the recipe as follows:

define :pear_module, :module => nil, :enable => true do
  include_recipe "php::pear"
  if params[:enable]
    execute "/usr/bin/pear install -a #{params[:module]}" do
      only_if "/bin/sh -c '! /usr/bin/pear info #{params[:module]} 2>&1 1>/dev/null"
    end
  end
end

By default, php maps to php::default, where default.rb is the default recipe for a cookbook. Please keep this in mind.

Cookbook Creation

Cookbooks are an amalgamation of recipes, definitions, templates, files etc. You should always have a metadata.rb or metadata.json file, which contains metadata about the file. If it is a complicated cookbook, feel free to include a README as well, in your preferred markup language (markdown is winning, fyi). You’ll also have one of several other filetypes, usually a recipe and a template, although they are all optional.

Once you’ve put together a template, it’s usually quite easy to integrate it with your other cookbooks. Just name the cookbook lower_under_score and shove it in your cookbooks directory. If you are feeling especially helpful, you can also upload it to the Opscore Community Site. Please run your cookbook before sharing, and also clearly state what other cookbooks they depend upon.

One last consideration is to make sure as much of your cookbook is configurable as possible, but with sane defaults. In this way, you’ll please the most users, while also encouraging “best” practices.

Recap

We now have a pretty awesome nginx_up resource that can be used across multiple cookbooks. We could go further and make an abstracted server_up resource, but I’m pretty happy with our progress for now.

So what’s next? Well, we have yet to deploy an entire server, and there is still the matter of what a DNA file actually is. As well, it would be useful to know how to actually push all of these files onto the server, and maintain the server as we move ahead. So the next post will cover the following:

  • Create a full-fledged cookbook
  • DNA files, how do they work?

We’ll get to the rest at a later date.

To Be Continued

blog comments powered by Disqus