Skip to Content

Building manageable server infrastructures with Puppet: Part 3

Posted on    13 mins read

About

In Part 2 of Building manageable server infrastructures with Puppet, we started to configure our puppetclient machine by writing a first, simple manifest on the Puppet master. We are now going to look at more complex configuration setups and we’ll learn how to put our manifests into a useful structure.

Modules

Keeping the managed configuration for a server infrastructure in just one file won’t scale well once this infrastructure grows beyond a handful of systems. An infrastructure with several servers that fullfill different roles can become complex, and we want to use Puppet to tame this complexity. To achieve this, we need to keep the information in our manifests clean and orderly.

One way to do this is to separate the manifests into modules. A module is a collection of one or more manifests that belong together because they serve the same puprose. A typical example for a module is a collection of manifests that enable a target machine to serve web pages through the Apache HTTP server. This module would assemble several manifests that take care of the different configuration aspects that need to be managed in order to provide a fully functional web server: software package installation, config file management, and service management.

We will create such a module now. Once finished, it will take care of providing a fully operational web server on the puppetclient machine.

Module folder structure

From an outside perspective, a Puppet module is simply a certain folder structure containing certain files. If you put the right files into the right folder structure, then the manifests within this structure can be referenced from within other manifests.

Let’s demonstrate this by moving the relevant parts from our first simple manifest – which we wrote in part 2 – into a module. Then, we will reference this modularized manifest from within our main manifest site.pp, instead of having the manifest declaration directly within site.pp.

Of course, our first manifest is just a “Hello World” example. Therefore, let’s call the module helloworld.

To do so, simply create a folder /etc/puppet/modules/helloworld/manifests on the puppetserver system:

On the puppetserver VM

~# sudo mkdir -p /etc/puppet/modules/helloworld/manifests

Besides the manifests themselves, modules also carry the files associated with them, in a subfolder files. Let’s create it, and move our helloworld.txt file into it:

On the puppetserver VM

~# sudo mkdir -p /etc/puppet/modules/helloworld/files
~# sudo mv /etc/puppet/files/helloworld.txt /etc/puppet/modules/helloworld/files/

Now we need to create the main manifest file of our new module. Put the following content into /etc/puppet/modules/helloworld/manifests/init.pp:

/etc/puppet/modules/helloworld/manifests/init.pp on puppetserver

class helloworld {

  file { "/home/ubuntu/helloworld.txt":
    ensure => file,
    owner  => "ubuntu",
    group  => "ubuntu",
    mode   => 0644,
    source => "puppet://puppetserver/modules/helloworld/helloworld.txt"
  }

}

As you can see, this is basically our original file configuration block, but it is now enclosed in a class block, and the path to the helloworld.txt file has changed. In Puppet manifests, a class block is the place where configuration blocks can be defined, for using them elsewhere later. That was not the case while the file block was part of our node definition – we defined it as part of the node, but it was also evaluated there. A class itself doesn’t actually do anything; it is set in motion only if it is declared elsewhere. We can declare our newly defined class within our node block by including it:

/etc/puppet/manifests/site.pp on puppetserver

node "puppetclient" {

  include helloworld

}

Instead of carrying the configuration block directly, our node definition now only references the block as defined in our new helloworld class, which in turn is part of our new helloworld module. Having all the helloworld-related configuration in a module gives us much more flexibility. Imagine we had a cluster of hundreds of nodes, and all of them are supposed to receive the helloworld configuration. We could copy-paste our node configuration block, but that wouldn’t scale well. The helloworld module, however, is reusable. It can be included in an arbitrary number of node blocks.

We have now restructured our existing configuration setup. That doesn’t change the actual configuration catalogue, however. Running the agent once again on the puppetclient system proves this:

On the puppetclient VM

~# sudo puppet agent --verbose --no-daemonize --onetime

info: Caching catalog for puppetclient
info: Applying configuration version '1396287576'
notice: Finished catalog run in 0.05 seconds

Because the expected configuration still matches the actual situation, no action is taken by the agent.

Here is an overview of the new structure in /etc/puppet regarding our manifests:

files

manifests
  site.pp

modules
  helloworld
    manifests
      init.pp
    files
      helloworld.txt

A first real module: apache2

Now, enough with those boring Hello World examples I say. Let’s set up a real web server on puppetclient.

We start by renaming the helloworld module, because we no longer need it. We are going to use the apache2 Ubuntu package to build our webserver, so we can as well call the new module apache2:

On the puppetserver VM

~# sudo mv /etc/puppet/modules/helloworld /etc/puppet/modules/apache2

A fully featured apache2 Puppet module has to take care of several aspects: it must ensure that certain software packages are installed, it must take care of several configuration files, it must make sure that the http daemon service is running (and restart it whenever there is a change to the server’s configuration).

The good news is that Puppet modules can be further divided into multiple classes. This helps us to keep those different concerns of a module cleanly separated. It’s not that Puppet itself needs this separation; it just makes it easier for us in the long run.

We will touch the existing init.pp manifest file of the module last. We will instead start by creating a manifest file called install.pp in folder /etc/puppet/modules/apache2/manifests. In this file, we will take care of having the right software packages installed:

/etc/puppet/modules/apache2/manifests/install.pp on puppetserver

class apache2::install {

  package { [ "apache2-mpm-prefork", "apache2-utils" ]:
    ensure => present
  }

}

This introduces a new block type, package. We can almost read its purpose in natural language from the way it is used: We use it to ensure that the packages apache2-mpm-prefork and apache2-utils are present on our target system.

Also, note the new syntax used for naming the class. The double-colon introduces a namespace into our class structure. The main class for the module is apache2 (we will use this class name in our init.pp manifest), but further classes exist that live in the apache2 namespace. They can be named however we like – install probably is a sensible name for a manifest that takes care of installation issues.

As explained earlier, Puppet does all the heavy lifting for us and utilizes apt-get in order to fullfill our expectation of having these software packages in question installed.

Now, installing these packages actually is enough if all you want is a running webserver. We will take it a step further, though, and will take care of the configuration of our webserver, too.

Templates and variables

To do so, let’s create the next class, apache2::config, in file /etc/puppet/modules/apache2/manifests/config.pp:

/etc/puppet/modules/apache2/manifests/config.pp on puppetserver

class apache2::config {

  file { "/etc/apache2/sites-available/${hostname}.conf":
    mode    => 0644,
    owner   => "root",
    group   => "root",
    content => template("apache2/etc/apache2/sites-available/vhost.conf.erb")
  }

}

This looks similar to the file block we already encountered, but also has some significant differences. It introduces two new concepts: variables and templates (and, as we will see, variables in templates).

Variables are a powerful tool and allow us to write more intelligent manifests. As you can see, the file path itself is parametrized through the hostname variable, which resolves to the fully qualified domain name of the target host. The hostname of our puppetclientsystem is, of course, puppetclient:

On the puppetclient VM

~# hostname

puppetclient

This means that when our manifest is applied on the target host, it will create the file /etc/apache2/sites-available/puppetclient.conf

hostname is one of many variables whose value Puppet determines itself. We will see how to define our own variables later in this series.

Another difference in our file block is the content statement. It differs from the source statement we encountered earlier in that it refers not to a static file, but to an embedded ruby template. A static file is simply transferred from the puppet master to its clients; a template file is dynamic and may contain variables (which are replaced with their values) and statements (which are evaluated).

Let’s look at file /etc/puppet/modules/apache2/templates/etc/apache2/sites-available/vhost.conf.erb on the puppetserver system to get an idea of how template files look:

/etc/puppet/modules/apache2/templates/etc/apache2/sites-available/vhost.conf.erb on puppetserver

<VirtualHost *:80>
  ServerName <%= hostname %>

  ServerAdmin webmaster@<%= hostname %>

  DocumentRoot /var/www

  <Directory />
    Options FollowSymLinks
    AllowOverride None
  </Directory>

  <Directory /var/www/>
    Options Indexes FollowSymLinks MultiViews
    AllowOverride All
    Order allow,deny
    allow from all
  </Directory>

  ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
  <Directory "/usr/lib/cgi-bin">
    AllowOverride None
    Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
    Order allow,deny
    Allow from all
  </Directory>

  ErrorLog ${APACHE_LOG_DIR}/<%= hostname %>.error.log
  LogLevel warn

  CustomLog ${APACHE_LOG_DIR}/<%= hostname %>.access.log combined
</VirtualHost>

As you can see, this is basically your average Apache virtual host file, but with placeholders. In this case, only one placeholder, <%= hostname %>. When the file is placed at the target file location on the target system, then every occurence of this placeholder will be replaced with the fully qualified domain name of the target system.

We could give our manifest a first try, but it has some imperfections we should first correct. The thing with Puppet manifests is that you can’t really predict the order of the application of their blocks. Puppet manifests are not batch files – they are not simply executed from top to bottom. Every block – like file or package – stands on its own. Puppet does try to decide on a sensible order of execution, but it’s not perfect. Where order is important, we need to assist Puppet.

What could possibly go wrong with our simple apache2 manifest in regards to execution order? Well, we install a software package, and we place a file into a folder that is created by the installation process for that software package. If Puppet would try to place the virtual host file into the sites-available folder before this folder has been created (by installing the apache2-mpm-prefork package), an error would occur.

We can explain Puppet, in our manifests, if manifest blocks depend on each other. To state that the file block for our virtual host file depends on the installation of package apache2-mpm-prefork being finished, we add a require statement to the block:

/etc/puppet/modules/apache2/manifests/config.pp on puppetserver

class apache2::config {

  file { "/etc/apache2/sites-available/${hostname}.conf":
    mode    => 0644,
    owner   => "root",
    group   => "root",
    content => template("apache2/etc/apache2/sites-available/vhost.conf.erb"),
    require => Package["apache2-mpm-prefork"]
  }

}

Note that when referencing a package, we write the Package statement with a capital P.

With this, Puppet knows how to decide on the order of the manifests, and it will ensure that the rules from the file block will only apply if the apache2-mpm-prefork package is installed on the system.

A first test run

We now have a first working version of our install and config manifests for our apache2 module. We now need to make sure that this module is applied on our puppetclient system. To do so, we first need to rewrite the main manifest of our module in /etc/puppet/modules/apache2/manifests/init.pp:

/etc/puppet/modules/apache2/manifests/init.pp on puppetserver

class apache2 {

  include apache2::install
  include apache2::config

}

Then, we need to map our new module to the puppetclient node in /etc/puppet/manifests/site.pp:

/etc/puppet/manifests/site.pp on puppetserver

node "puppetclient" {

  include apache2

}

On the puppetclient VM

~# sudo puppet agent --verbose --no-daemonize --onetime

info: Caching catalog for puppetclient
info: Applying configuration version '1396980729'
notice: /Stage[main]/Apache2::Install/Package[apache2-utils]/ensure: ensure changed 'purged' to 'present'
notice: /Stage[main]/Apache2::Install/Package[apache2-mpm-prefork]/ensure: ensure changed 'purged' to 'present'
notice: /Stage[main]/Apache2::Config/File[/etc/apache2/sites-available/puppetclient.conf]/ensure: defined content as '{md5}c55c5bd945cea21c817bca1a465b7dd3'
notice: Finished catalog run in 16.62 seconds

Well, this worked. But we have not yet reached our goal. The virtual host puppetclient needs to be activated. To achieve this, we only need a symbolic link from /etc/apache2/sites-enabled/puppetclient.conf to /etc/apache2/sites-available/puppetclient.conf. Another file type block makes this possible:

/etc/puppet/modules/apache2/manifests/config.pp on puppetserver

class apache2::config {

  file { "/etc/apache2/sites-available/${hostname}.conf":
    mode    => 0644,
    owner   => "root",
    group   => "root",
    content => template("apache2/etc/apache2/sites-available/vhost.conf.erb"),
    require => Package["apache2-mpm-prefork"]
  }

  file { "/etc/apache2/sites-enabled/${hostname}.conf":
    ensure  => link,
    target  => "/etc/apache2/sites-available/${hostname}.conf",
    require => File["/etc/apache2/sites-available/${hostname}.conf"]
  }

}

We apply the new manifest:

On the puppetclient VM

~# sudo puppet agent --verbose --no-daemonize --onetime

info: Caching catalog for puppetclient
info: Applying configuration version '1396983687'
notice: /Stage[main]/Apache2::Config/File[/etc/apache2/sites-enabled/puppetclient.conf]/ensure: created
notice: Finished catalog run in 0.07 seconds

…and we are done, right? Well, in a sense, we are. Apache is installed, and our virtual host has been configured. With Puppet, however, we can do even better. What would we do if we managed the puppetclient manually? After changing the Apache configuration, we would test if it contains any errors (using apachectl configtest, and if not, we would restart the apache2 service. Turns out, Puppet can do the same.

Refining the module

To make this work, we first need to teach Puppet about the apache2 service. To do so, we need to create another manifest file on the puppetserver system at /etc/puppet/modules/apache2/manifests/service.pp:

/etc/puppet/modules/apache2/manifests/service.pp on puppetserver

class apache2::service {

  service { "apache2":
    ensure     => running,
    hasstatus  => true,
    hasrestart => true,
    restart    => "/usr/sbin/apachectl configtest && /usr/sbin/service apache2 reload",
    enable     => true,
    require    => Package["apache2-mpm-prefork"]
  }

}

As you can see, Puppet ships with a type service that knows how to handle daemon applications. With this manifest, we ask Puppet to take charge of the apache2 service. Whenever the Puppet agent runs on the target system, it will make sure that this service is running. Also, Puppet is able to restart services, and in this case, we even teached it to restart the apache2 service only if apachectl configtest returns no errors. This allows for fully automated service management without giving up the safety that manual management provides.

Of course, we need to declare the new class in our main mainfest file:

/etc/puppet/modules/apache2/manifests/init.pp on puppetserver

class apache2 {

  include apache2::install
  include apache2::config
include apache2::service
}

The last thing to do is to connect our config with our service manifest, because we would like to trigger an Apache restart whenever we change our virtual host configuration. This is achieved with the notify statement:

/etc/puppet/modules/apache2/manifests/config.pp on puppetserver

class apache2::config {

  file { "/etc/apache2/sites-available/${hostname}.conf":
    mode    => 0644,
    owner   => "root",
    group   => "root",
    content => template("apache2/etc/apache2/sites-available/vhost.conf.erb"),
    require => Package["apache2-mpm-prefork"],
    notify  => Service["apache2"]
  }

  file { "/etc/apache2/sites-enabled/${hostname}.conf":
    ensure  => link,
    target  => "/etc/apache2/sites-available/${hostname}.conf",
    require => File["/etc/apache2/sites-available/${hostname}.conf"]
  }

}

Let’s see if this works as intented. Simply change the virtual host template file at /etc/puppet/modules/apache2/templates/etc/apache2/sites-available/vhost.conf.erb (adding a space or empty line does the job), and then re-run the agent on the client. I have highlighted the interesting parts:

On the puppetclient VM

~# sudo puppet agent --verbose --no-daemonize --onetime

info: Caching catalog for puppetclient
info: Applying configuration version '1396998784'
info: FileBucket adding {md5}c55c5bd945cea21c817bca1a465b7dd3
info: /Stage[main]/Apache2::Config/File[/etc/apache2/sites-available/puppetclient.conf]: Filebucketed /etc/apache2/sites-available/puppetclient.conf to puppet with sum c55c5bd945cea21c817bca1a465b7dd3
notice: /Stage[main]/Apache2::Config/File[/etc/apache2/sites-available/puppetclient.conf]/content: content changed '{md5}c55c5bd945cea21c817bca1a465b7dd3' to '{md5}afafea12b21e61c5e18879ce3fe475d2'
info: /Stage[main]/Apache2::Config/File[/etc/apache2/sites-available/puppetclient.conf]: Scheduling refresh of Service[apache2]
notice: /Stage[main]/Apache2::Service/Service[apache2]: Triggered 'refresh' from 1 events
notice: Finished catalog run in 0.32 seconds

Let’s also check if the safety mechanism works as expected. Again, edit the virtual host template file, and make it invalid, e.g. by removing the closing > from one of the tags.

The agent will react as expected:

On the puppetclient VM

~# sudo puppet agent --verbose --no-daemonize --onetime

info: Caching catalog for puppetclient
info: Applying configuration version '1396998943'
info: FileBucket adding {md5}d15f727106b64c29d1c188efc9e6c97d
info: /Stage[main]/Apache2::Config/File[/etc/apache2/sites-available/puppetclient.conf]: Filebucketed /etc/apache2/sites-available/puppetclient.conf to puppet with sum d15f727106b64c29d1c188efc9e6c97d
notice: /Stage[main]/Apache2::Config/File[/etc/apache2/sites-available/puppetclient.conf]/content: content changed '{md5}d15f727106b64c29d1c188efc9e6c97d' to '{md5}7c86c7ba3cd4d7522d36b9d9fdcd46e2'
info: /Stage[main]/Apache2::Config/File[/etc/apache2/sites-available/puppetclient.conf]: Scheduling refresh of Service[apache2]
err: /Stage[main]/Apache2::Service/Service[apache2]: Failed to call refresh: Could not restart Service[apache2]: Execution of '/usr/sbin/apachectl configtest && /usr/sbin/service apache2 reload' returned 1:  at /etc/puppet/modules/apache2/manifests/service.pp:10
notice: Finished catalog run in 0.19 seconds

Don’t forget to fix the template file so it’s working again.

Conclusion and outlook

Using templates, variables, dependencies and notifications, building full-fledged and robust manifests is straightforward and simple. Using modularization makes manifests re-usable for a wide range of clients. In part 4 of this series we discuss how parametrization allows us to make our modules even more flexible.