Templates
Chef dynamically configures software and hosts, and a large part of configuring UNIX-based systems and software involves configuration files. Chef provides a straightforward mechanism to generate configuration files that make it easy to combine configuration data with template files to produce the final configuration files on hosts. These templates are stored in the templates
directory inside of a cookbook and use the ERB template language, which is a popular and easy-to-use Ruby-based template language.
Why use templates?
Without templates, your mechanism to generate configuration files would probably look something like this:
File.open(local_filename, 'w') do |f| f.write("<VirtualHost *:#{node['app]['port']}") ... f.write("</VirtualHost>") end
This should be avoided for a number of reasons. First, writing configuration data this way would most likely make your recipe very cluttered and lengthy. Secondly, and more importantly, it violates Chef's declarative nature. By design, Chef provides you with the tools to describe what the recipe is doing and not how it is doing it, which makes reading and writing recipes much easier. Simpler recipes make for simpler configuration, and simpler configuration scales better because it is easier to comprehend. Handrolling a configuration file is the opposite approach; it very specifically dictates how to generate the file data. Consider the following code:
config_file = "#{node['postgresql']['dir']}/postgresql.conf" pgconfig = node[:postgresql] File.open(config_file 'w') do |f| f.write("port = #{pgconfig[:port]}") f.write("data_dir = #{pgconfig[:data_dir]}") f.write("listen_address = #{pgconfig[:listen_address]}") end File.chown(100,100,config_file) File.chmod(0600, config_file)
This code generates a PostgreSQL configuration file from the attribute hash, one line at a time. This is not only time-consuming and hard to read but potentially very error-prone. You can imagine, even if you have not previously configured any PostgreSQL servers, just how many f.write(...)
statements could be involved in generating a full postgresql.conf
file by hand. Contrast that with the following block that leverages the built-in template
resource:
template "#{node['postgresql']['dir']}/postgresql.conf" do source "postgresql.conf.erb" owner "postgres" group "postgres" mode 0600 end
The preceding block could be combined with a template file that contains the following content:
<% node['postgresql'] sort.each do |key, value| %> <% next if value.nil? -%> <%= key %> = <%= case value when String "'#{value}'" when TrueClass 'on' when FalseClass 'off' else value end %> <% end %>
If we take our template and then apply the following attribute data as we had shown previously, then we would have generated the exact same configuration file:
'node' : { 'postgresql': { 'port': '5432', 'listen_address': '*', 'data_dir': '/var/lib/postgresql', 'install_method': 'source', 'version': '9.3' }, 'users' : { 'root' : { 'group' => 'wheel' }, } }
Only now we can use a template that is highly flexible. Our template uses the key-value combinations stored in the configuration hash to dynamically generate the postgresql.conf
file without being changed every time a new configuration option is added to PostgreSQL.
Chef uses ERB, a template language, that is provided by the core Ruby library. ERB is widely available and requires no extra dependencies; it supports Ruby inside of templates as well as some ERB-specific template markup.
As ERB is very well documented and widely used, this portion of the chapter serves only as a quick reference to some of the most commonly used ERB mechanisms. For more information, see the official Ruby documentation at http://ruby-doc.org/stdlib-2.1.1/libdoc/erb/rdoc/ERB.html.
To execute some arbitrary Ruby code, you can use the <% %>
container. The <%
part indicates the beginning of the Ruby code, and %>
indicates the end of the block. The block can span multiple lines or just one single line. Examples of this are as follows:
You can mix Ruby and non-Ruby code (useful to repeat blocks of non-Ruby text) as follows:
<% [1,2,3].each do |value| %> Non-ruby text... <% end %>
This would yield the following:
Non-ruby text... Non-ruby text... Non-ruby text...
ERB has a syntax to replace a section of the template with the results of some Ruby code rather than relying on print statements inside your Ruby. That container is similar to the last one, with the addition of the equal sign inside the opening tag. It looks like <%= %>
. Any valid Ruby code is acceptable inside this block, and the result of this code is put into the template in place of the block. Examples of this are as follows:
<%= @somevariable %> <%= hash[:key] + otherhash[:other_key] %> <%= array.join(", ") %>
This can be combined with the previous example to produce complex output:
<% [1,2,3].each do |value| %> The value currently is <%= value %> <% end %>
This would yield the following:
The value currently is 1 The value currently is 2 The value currently is 3
Using just these basic features of ERB, combined with Chef's configuration data, you can model just about any configuration file you can imagine.
The template resource
Chef provides a template resource to generate files via templates. There are three key attributes of the template resource, which are as follows:
path
: This specifies where to put the generated filesource
: This tells the resource which template file to usevariables
: This specifies what data to populate the template with
The path
attribute uses the name attribute as its default value and populates the template specified by a source
file with the data passed to it through the variables
attribute. Templates are contained inside of the templates
directory, which is placed inside of a cookbook; if a source is not specified, it will be expected that a file exists inside the directory with the same name as the path, which is only rooted in the templates
directory with a .erb
extension. Here is a simple template
resource example:
template "/etc/motd" do variables :users => ["Bart", "Homer", "Marge"] end
This resource will expect that a file exists in the template's search path (more on how that is determined in a bit) as etc/motd.erb
, and it then exposes an array of three strings as a users
variable and writes the results out as /etc/motd
on the host. The corresponding MOTD template could look like the following:
Welcome to crabapple.mydomain.com! Our newest users are: <% @users.each do |user| %> * <%= user %> <% end %>
The template variables
There are two primary sources of data for a template: data passed explicitly through the resource block attributes and node configuration data. Explicit variables are user defined in the recipe and may be used to override some settings or pass in configuration that is dynamically generated inside the recipe. The node configuration data is computed by Chef at runtime and represents a snapshot of the current configuration that will be applied to the node.
Sometimes you will need to pass data to a template from inside your recipe instead of relying on the global node attributes. Perhaps you have some logic that computes some variable data, but it doesn't belong in the node hash; Chef supports doing just this in the template
resource. The data passed explicitly is available to the ERB template as an instance variable, prefixed in Ruby with the @
symbol. For example, consider the following recipe snippet:
config_hash = { :food => "asparagus", :color => "blue" } template "/etc/myapp.conf" do source "myapp.conf.erb" owner "root" mode "0664" variables( :install_path => "/opt/#{hostname}/myapp", :config => config_hash ) end
The :install_path
and :config
keys are accessible in the template as instance variables with the same name. They will be prefixed by the @
character and could be used in a template similar to the following:
database_path = "<%= @install_path %>/db" storage_path = "<%= @install_path %>/storage" <% config.each do |key,value| %> <%= key %> = "<%= value %>" <% end %>
Here, the template expects a specific key, install_path
, to determine where to store the database; the key-value hash specified by config
is then used to generate some arbitrary configuration settings in the template.
In addition to data passed via the variables
attribute, a template can access a node's computed configuration data through the node
local variable. This is accessed as a Ruby hash, which will be structured similarly to a dictionary or a hash in any other language. In our previous PostgreSQL attribute's data example, the following values were defined:
default['postgresql']['port'] = "5432" default['postgresql']['listen_address'] = "*"
Even if no other configuration data supersedes these configuration values, there will be a postgresql
key in the node's configuration data that contains the key's port
and listen_address
. Using this information, we can write a recipe that uses a template resource and a matching template like the following:
template "/etc/postgresql/postgresql.conf" do source "postgresql.conf.erb" owner "psql" mode "0600" end listen_addresses = '<%= node[:postgresql][:listen_address]' port = <%= node[:postgresql][:port] %>
When the default attributes data is combined with the example template, the resulting /etc/postgresql/postgresql.conf
file will have the following content:
listen_addresses = '*' post = 5432
As previously discussed, the computed attributes hash for a given node comes from a variety of sources. Those sources include attributes files in the cookbook, role, environment, and node-level configuration values stored in Chef, each with its own level of precedence.
Searching for templates
As you have likely noticed, Chef attempts to allow you as much, or as little, specificity as you want when defining your configuration, and templates are no different. Just as the final node configuration is computed from a variety of locations, the templates
directory has a specific search order. This allows the author of the cookbook to provide a set of default templates as well as support platform and host-specific overrides.
The default
template directory should be used to provide default versions of the templates. Any platform- or host-specific directories are placed alongside it and will be used when appropriate. The search order for a template is as follows:
- Hostname
- Distribution version
- Distribution
- Default location
As an example, let's consider a scenario in which we applied a recipe with the postgresql.conf.erb
template resource to a node, db1.production.mycorp.com
, which is running Debian 6.0. Chef will then look for the following files inside of the templates directory:
host-db1.production.mycorp.com/postgresql.conf.erb
debian-6.0/postgresql.conf.erb
debian/postgresql.conf.erb
default/postgresql.conf.erb
The search is performed in that order with the first match being the chosen template, applying the highest level of specificity before the lowest (as is the pattern with other Chef mechanisms, including configuration data).
This differentiation of configuration files by host, platform, and even version is very useful. It allows you to provide a sane set of defaults while supporting host- or system-specific quirks simultaneously.