Templating dynamic configuration files
In the introduction, I stated that the techniques that you are now learning are not frequently required. That was true, except for this one topic. Templates are actually a cornerstone of configuration management with Puppet.
Templates are an alternative way to manage configuration files or any files, really. You have synchronized files from the master to an agent that handled some Apache configuration settings. These are not templates, technically. They are merely static files that have been prepared and are ready for carbon copying.
These static files suffice in many situations, but sometimes, you will want the master to manage very specific configuration values for each agent. These values can be quite individual. For example, an Apache server usually requires a MaxClients
setting. Appropriate values depend on many aspects, including hardware specifications and characteristics of the web application that is being run. It would be impractical to prepare all possible choices as distinct files in the module.
Learning the template syntax
Templates make short work of such scenarios. If you are familiar with ERB templates already, you can safely skip to the next section. If you know your way around PHP or JSP, you will quickly get the hang of ERB—it's basically the same but with Ruby inside the code tags. The following template will produce Hello,world!
three times:
<% ( 1 .. 3 ).each do %> Hello, world! <% end %>
This template will also produce lots of empty lines, because the text between the <%
and %>
tags gets removed from the output but the final line breaks do not. To make the ERB engine do just that, change the closing tag to -%>
:
<% ( 1 .. 3 ).each do -%> Hello, world! <% end -%>
This example is not very helpful for configuration files, of course. To include dynamic values in the output, enclose Ruby expressions in a <%=tag
pair:
<% ( 1 .. 3 ).each do |index| -%> Hello, world #<%= index %> ! <% end -%>
Now, the iterator value is part of each line of the output. You can also use member variables that are prefixed with @
.
These variables are populated with the values from the Puppet manifest variables:
<IfModule mpm_worker_module> ServerLimit <%= @apache_server_limit %> StartServers <%= @apache_start_servers %> MaxClients <%= @apache_max_clients %> </IfModule> <% @apache_ports.each do |port| -%> Listen <%= port %> NameVirtualHost *:<%= port %> <% end -%>
Variables that are used in a template must be defined in the same scope or scopes from which the template is used. The next section explains how this works.
In Puppet 3.x, variable values are mostly strings, arrays, or hashes. To write efficient templates, it is helpful to occasionally glance at the methods available for the respective Ruby classes. In Puppet 4, variables have more diverse values.
There are several ways to use Puppet variables in templates:
- Prefixing the variable with the
@
sign: This means that the variable is global, or it was defined in the same class where the template is used. This works with Puppet 2.7, Puppet 3, and Puppet 4. - Using the
scope.lookupvar('variablewithscopename')
function: This allows you to refer to any variable in any class of the module. Please do not look up variables in other modules; it will build an invisible dependency on the other module. The syntax works with Puppet 2, Puppet 3, and Puppet 4. - Using
scope['variablewithscope']
: In Puppet 3, the scope hash can be used directly. The behavior is similar toscope.lookupvar
. This will work with Puppet 3 and Puppet 4.
Using templates in practice
Templates have their own place in modules. You can place them freely in the templates/
subtree of the module. The template
function locates them using a simple descriptor:
template('cacti/apache/cacti.conf.erb')
This expression evaluates the content of the template found in modules/cacti/templates/apache/cacti.conf.erb
. The first path element (without a leading slash) is the module name. The rest of the path gets translated to the templates/
tree in the module. The function is commonly used to generate the value of a file
resource's content
property:
file { '/etc/apache2/conf.d/cacti.conf': content => template('cacti/apache/cacti.conf.erb'), }
Many templates expect some variables to be defined in their scope. The easiest way to make sure that this happens is to wrap the respective file
resource in a parameterized container. Files that are singletons with a well-known name, such as /etc/ssh/sshd_config
, should be managed through a parameterized class. Configuration items that can inhabit multiple files, such as /etc/logrotate.d/*
or /etc/apache2/conf.d/*
, are well suited to be wrapped in defined types:
define logrotate::conf( String $pattern, Integer $max_days=7, Array $options=[] ) { file { "/etc/logrotate.d/$name": mode => '0644', content => template('logrotate/config-snippet.erb') } }
In the preceding example, the template uses the parameters as @pattern
, @max_days
, and @options
, respectively.
For a quick and dirty string transformation of your data, you can also use the inline_template
function in your manifest. This is often found on the right-hand side of a variable assignment:
$comma_seperated_list = inline_template('<%= @my_array * "," %>')
This example assumes that the $my_array
Puppet variable holds an array value.
Avoiding performance bottlenecks from templates
When using templates, both through the template
and inline_template
functions, be aware that each invocation implies a performance penalty for your Puppet master. During the compilation of the catalog, Puppet must initialize the ERB engine for any template it encounters. The ERB evaluation happens in an individual environment that is derived from the respective scope of the template
function invocation.
It is, therefore, not even important how complex your templates are. If your manifest requires frequent expansion of a very short template, it generates an enormous overhead for each initialization. Especially in the case of an easy inline_template
function, such as the one mentioned previously, it can be worthwhile to invest some more effort in creating a parser function instead, as seen in Chapter 5, Extending Your Puppet Infrastructure with Modules. A function can perform variable value transformation without incurring the cumulative penalty.
On the bright side, using templates is quite economic for the agent, who receives the whole textual file content right inside the catalog. There is no need to make an additional call to the master and retrieve file metadata. On a high-latency network, this can be a noticeable saving.
There is no silver bullet here. Don't let the performance implications deter you from turning specific configuration files into templates. Template-based solutions will often make your module more maintainable, which will usually offset performance implications—hardware is constantly getting cheaper, after all. Just don't be wasteful with frequent (and simple) expansions.