Skip to Content

Building manageable server infrastructures with Puppet: Part 2

The second part of my Puppet series explains how to use the infrastructure that was set up in part 1 in order to automatically and centrally manage the configuration of a Puppet client system.

Posted on 7 mins read

About

In Part 1 of Building manageable server infrastructures with Puppet, we have set up two virtual Linux systems, a puppetserver and a puppetclient. We reached an important first milestone: We installed and set up the Puppet server and Puppet client software on their respective machines, and we authenticated the Puppet client with the Puppet server. We are now going to use this setup to start configuring our puppetclient system through the Puppet server on the puppetserver system.

Hello Puppet World

Let’s start with a very basic example. We will set up a configuration for our puppetclient that is really simple: The result will be a plain text file named helloworld.txt containing the phrase “Hello World!” being created in the directory /home/ubuntu on the puppetclient system.

Puppet ships with a powerful declarative configuration language. We use this language to write so-called manifests. A puppet manifest is a file that describes how a certain aspect of a target system should look like. In the course of this series, we will write many different manifests: some of them will result in files being created on the target system; some will result in user accounts being created; some will install software packages.

Manifests are applied onto target systems. In Puppet, target systems are called nodes. Our puppetclient system is such a node. We have already authenticated the system with the Puppet server; this enables the server to serve the node. But our server does not yet have any information on what to serve this node. Let’s change this by writing a manifest and associating this manifest with our node.

To do so, we will put a very basic manifest definition into the main manifest file on the puppetserver system, /etc/puppet/manifests/site.pp:

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

node "puppetclient" {

  file { "/root/helloworld.txt":
    ensure => file,
    owner  => "root",
    group  => "root",
    mode   => 0644
  }

}

As we will see later, manifests can be modularized into an arbitrary number of different files (which helps to give structure to the manifests of a large and complex site), but everything starts with site.pp.

Let’s dissect this minimal manifest. It consists of two sections: a node which contains a file definition. Because the file section is contained within the node section, the actions that are the result of the file section’s definition will apply to the node named puppetclient.

What this means in practice can easily be demonstrated, by running the Puppet agent on the puppetclient system:

On the puppetclient VM

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

info: Caching catalog for puppetclient
info: Applying configuration version '1395862307'
notice: /Stage[main]//Node[puppetclient]/File[/home/ubuntu/helloworld.txt]/ensure: created
notice: Finished catalog run in 0.03 seconds

It’s probably not a big surprise: The Puppet agent on system puppetclient contacted the Puppet master on system puppetserver. It then retrieved the catalog, that is, all the manifest definitions on the master that are relevant for this particular client. In case you are curious, the catalog is saved as a yaml structure in file /var/lib/puppet/client_yaml/catalog/puppetclient.yaml – it’s not simply a copy of our .pp manifest, but rather a compiled version of the target configuration that was created by parsing our manifest.

Then, the Puppet agent takes action – if, and only if, action has to be taken: It compares the situation that our manifest expects with the actual situation on the target node. If there is a delta – if the situation found on the target node is not in sync with the situation that is to be achieved – then the agent does whatever neccessary to remove that delta, to reach the target situation.

In our particular case, the agent learned that a file named helloworld.txt with owner and group ubuntu and mode 0644 is expected to exist at location /home/ubuntu. However, when checking the local system, no such file is found. The agent takes action and creates the file that is expected to exist.

We can verify this behaviour by running the agent again:

On the puppetclient VM

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

info: Caching catalog for puppetclient
info: Applying configuration version '1395862542'
notice: Finished catalog run in 0.03 seconds

As we can see, the agent simply does nothing, because the target situation is already satisfied. What if we change the situation on the target node? Let’s change the mode of the file:

On the puppetclient VM

~# chmod 0640 /home/ubuntu/helloworld.txt

Run the agent again:

On the puppetclient VM

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

info: Caching catalog for puppetclient
info: Applying configuration version '1395862542'
notice: /Stage[main]//Node[puppetclient]/File[/home/ubuntu/helloworld.txt]/mode: mode changed '0640' to '0644'
notice: Finished catalog run in 0.03 seconds

The agent noticed the delta, and removed it by changing the mode of the file.

What if the change the content of the file…

On the puppetclient VM

~# echo "This is a test" > /home/ubuntu/helloworld.txt

…and run the agent?

On the puppetclient VM

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

info: Caching catalog for puppetclient
info: Applying configuration version '1395862542'
notice: Finished catalog run in 0.03 seconds

Nothing happens. Why? Because in our manifest, we didn’t say anything about the contents of the file. All we did was, we asked Puppet to ensure that a file with the given name exists, and how it’s metadata (owner, group, mode) should look like. Puppet only takes care of what it is asked to take care of.

The behaviour we witnessed in the preceding example lies at the very heart of Puppet’s philosophy. In our manifests, we don’t tell Puppet what to do. We don’t tell it how to do it. We only tell Puppet what the end result should look like.

This philosophy carries great power, because it abstracts away the grunt work needed to configure systems. It levels the differences between different operating systems. Imagine a situation where you have a network of different Linux systems – some run Red Hat Linux, some run Ubuntu Linux. Let’s further assume that you would like to have the package htop installed on all systems. If we had to tell Puppet what to do and how to do it, we would have to write a manifest that talks about apt-get for the Ubuntu systems and yum for the Red Hat systems. Instead, all we need to put into a manifest is this:

package { "htop":
  ensure => installed
}

The puppet agent on the target nodes will figure out how to reach the target situation described in this manifest. An agent on a Red Hat system will use yum to install the package; an agent on an Ubuntu system will use apt-get.

We will get back to package installation later. Let’s continue with our file example. Creating empty files through Puppet isn’t of very much use – of course we would like to deploy files with content. Puppet makes this easy: we can place files on the puppetserver and have them transferred onto the puppetclient through Puppet.

First, let’s create the source file on the puppetserver:

On the puppetserver VM

~# sudo -s
~# mkdir /etc/puppet/files
~# echo "Hello World." > /etc/puppet/files/helloworld.txt

Then, we need to allow access to the files folder on the server for Puppet clients. For this, we need to change /etc/puppet/fileserver.conf on the puppetserver by adding an allow * statement:

/etc/puppet/fileserver.conf on puppetserver

# This file consists of arbitrarily named sections/modules
# defining where files are served from and to whom

# Define a section 'files'
# Adapt the allow/deny settings to your needs. Order
# for allow/deny does not matter, allow always takes precedence
# over deny
[files]
  path /etc/puppet/files
  allow *
#  allow *.example.com
#  deny *.evil.example.com
#  allow 192.168.0.0/24

[plugins]
#  allow *.example.com
#  deny *.evil.example.com
#  allow 192.168.0.0/24

Now we can extend our existing file block in the manifest file vim /etc/puppet/manifests/site.pp:

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

node "puppetclient" {

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

}

Let’s run the agent on the client system once again:

On the puppetclient VM

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

info: Caching catalog for puppetclient
info: Applying configuration version '1395878127'
info: FileBucket adding {md5}ff22941336956098ae9a564289d1bf1b
info: /Stage[main]//Node[puppetclient]/File[/home/ubuntu/helloworld.txt]: Filebucketed /home/ubuntu/helloworld.txt to puppet with sum ff22941336956098ae9a564289d1bf1b
notice: /Stage[main]//Node[puppetclient]/File[/home/ubuntu/helloworld.txt]/content: content changed '{md5}ff22941336956098ae9a564289d1bf1b' to '{md5}770b95bb61d5b0406c135b6e42260580'
notice: Finished catalog run in 0.09 seconds

Now the Puppet agent does care about the contents of the file and overwrites the existing file content with the content from the file on the puppetserver.

In part 3 of Building manageable server infrastructures with Puppet we will look at more complex manifests, and how to structure our manifests into modules.