Puppet Quickstart

published on in category Puppet , Tags: Provisioning Puppet

In the following quick start guide I will show you the basic usage of Puppet. If you want to follow me along, you’ll only need the text editor of your choice (I’m using Sublime Text here) and a command line. Also you should be using a supported operating system, I’ll describe the installation exemplary on Fedora Linux and Mac OS X Yosemite.

Some theory

What is Puppet?

Puppet is a tool for managing system configurations. Although Puppet can primarily be found in large server environments, where many machines need to be set up the exact same way, it could possibly also be used for setting up your own workstation or configuring a switch.

Written in Ruby, Puppet is generally platform independent but only *NIX alike systems like Linux and BSD are supported officially.

Manifest files

Puppet features a relatively easy configuration syntax. The administrator simply creates a text file containing the desired state for a given system. The state is defined in a declarative manner, like:

The file /etc/resolv.conf is owned by user root.

When starting the Puppet command line tool, it will double-check all assertions and apply all the changes required to get into the desired state as specified.

By implication this means that if everything is already configured properly, Puppet will do absolutely nothing.

Modules

Puppet maintains a large repository of ready-to-use modules called Puppet Forge. A lot of modules can also be found on GitHub. So for instance if you need a module to configure the Apache web server or a package provider for Homebrew, you’ll likely find one there.

Standalone & Puppetmaster

Generally, Puppet can be operated in two modes: Standalone (agent) or using the client/server principle. In the larger setups, Puppet is meant to run in, you’ll find a Puppet server called puppetmaster which holds all relevant manifests and modules. Every agent (node) authenticates against the puppetmaster using public key authentication.

The agent then runs in the background on every node and periodically checks for changes regarding the manifest files and applies them if necessary.

Using the node definition, a manifest can contain different instructions for each client. The nodes themselves are identified by their individual host names.

However, in this guide we’ll focus on the standalone mode only. This mode doesn’t require a puppetmaster because the manifest and all modules are available on the local machine and manually applied by running the puppet apply command.

Installing Puppet

Mac OS X

Puppet requires Ruby in version 1.8 or higher, which is already the case for Mavericks and Yosemite. Using Ruby’s package manager gem, Puppet can be installed right from the command line:

sudo gem install puppet

Puppet’s own dependencies named facter and heira will get installed automatically.

Fedora Linux

Most Linux distributions maintain own packages for Puppet in their respective package management system, which is also the case for Fedora. Therefore Puppet can be installed using the following command:

sudo yum install puppet

On my Fedora 21 box, yum installed version 3.6.2:

puppet --version
3.6.2

Writing the first manifest file

Example: Creating a user

Below we’ll create a Puppet manifest that creates a system user as well as a file. Afterwards we change the ownership of the create file to the created user. While this has not much in common with usual server administration tasks, it explains the basic syntax usage very well.

Create a file called test.pp with the following contents:

user { "puppet_test":
        ensure => present,
}

All state definitions are done this way in Puppet. user is a type. Within the curly brackets, the block first gets a name (puppet_test) followed by various key -> value definitions called attributes. There are some attributes that all types have in common, e.g. ensure or require. Other attributes depend on the type you are using. A complete list can be found in the type reference.

Now we can run puppet apply to apply our manifest:

puppet apply test.pp
Could not retrieve fact='virtual', resolution='': Permission denied @ rb_sysopen - /sys/firmware/dmi/entries/1-0/raw
Notice: Compiled catalog for rabbithole-local.local in environment production in 0.24 seconds
Error: Could not find a suitable provider for user
Notice: Finished catalog run in 0.14 seconds

As you can see, the execution didn’t work in the above case. This is the case, because we didn’t run Puppet as a privileged user, which is needed to gather all relevant system information.

Running the same command using sudo solves this problem:

sudo puppet apply test.pp
Notice: Compiled catalog for rabbithole-local.local in environment production in 0.12 seconds
Notice: /Stage[main]/Main/User[puppet_test]/ensure: created
Notice: Finished catalog run in 0.44 seconds

Now the command was executed successfully. The line /Stage[main]/Main/User[puppet-test]/ensure: created indicates, that the user was created successfully.

If we’d run puppet apply again, nothing would happen because the user already exists. Simple, huh?

sudo puppet apply test.pp
Notice: Compiled catalog for rabbithole-local.local in environment production in 0.12 seconds
Notice: Finished catalog run in 0.07 seconds

Dependencies

In the next step, we’ll create a file and fill it with example contents. This can be accomplished using the type file. Append the following lines to the test.pp file:

file { "fancy file":
    ensure => present,
    path => "/home/spike/Desktop/puppet_test_file",
    content => "Hello World",
    owner => "puppet_test",
    require => User["puppet_test"],
}

As you can see, we used the same scheme as before. The name “fancy file” does not matter at all in this case and is only used for referencing this block.

The file may have the contents “Hello World” and belong to puppet_test. Using the require attribute we specified a dependency between the file and the user because it’s ownership cannot be changed, if the user was not created first.

Explicitly defining those requirements is a absolutely essential part in Puppet because the manifest file is not interpreted on a per-line basis but arranged in the right order after resolving all dependencies (if not circular because this doesn’t work).

We’ll now run Puppet again:

sudo puppet apply test.pp
Notice: Compiled catalog for rabbithole-local.local in environment production in 0.20 seconds
Notice: /Stage[main]/Main/File[cool file]/ensure: created
Notice: Finished catalog run in 0.11 seconds

As you see, the file has been created. Due to the fact that the user is already existing, Puppet won’t mention it anymore.

Taking a look into the target directory reveals that the file has been created:

ls -l puppet_test_file
-rw-r--r--. 1 puppet_test root 10  2. Jan 19:29 puppet_test_file

Apply state corrections

Let’s change the file ownerships by hand:

sudo chown spike puppet_test_file
ls -al puppet_test_file    
-rw-r--r--. 1 spike root 10  2. Jan 19:29 puppet_test_file

By running Puppet again, only the ownership will be corrected because this is the only thing that differs from the manifests’ state.

sudo puppet apply test.pp
Notice: Compiled catalog for rabbithole-local.local in environment production in 0.19 seconds
Notice: /Stage[main]/Main/File[cool file]/owner: owner changed 'spike' to 'puppet_test'
Notice: Finished catalog run in 0.06 seconds

As you see, Puppet is able to check individual aspects of existing setups and change only those, who are not configured as intended.

Classes: Grouping blocks

As you might have recognized by now, the configuration of a whole server may easily get unclear. This is why Puppet supports grouping blocks to classes in order to structure functional aspects of your manifest.

Let’s create a class called create_user_and_file that contains both blocks we created before. The whole file will look like this:

class create_user_and_file {
    user { "puppet_test":
        ensure => present,
    }

    file { "fancy file":
        ensure => present,
        path => "/home/spike/Desktop/puppet_test_file",
        content => "Hello World",
        owner => "puppet_test",
        require => User["puppet_test"],
    }
}

Additionally you could outsource this functionality into a separate file, but for now we’ll keep it right where it is.

If you’d run puppet apply again, even if you changed something in the expected configuration, nothing would happen. This is because even though the class is properly defined, it’s not executed in any context. Like in object oriented programming, defining a class creates a blueprint for a object but without creating a object, no logic is executed.

So we need to include the class to our root manifest again. Append this to the test.pp file:

include create_user_and_file

By running Puppet, our configuration will be applied again:

sudo puppet apply test.pp
Notice: Compiled catalog for rabbithole-local.local in environment production in 0.25 seconds
Notice: /Stage[main]/Create_user_and_file/File[fancy file]/ensure: created
Notice: Finished catalog run in 0.34 seconds

Nodes

Let’s assume, we have multiple machines we want to configure using the same manifest. In this example, we’ll modify our manifest to only apply our class create_user_and_file to the host called rabbithole-local. So let’s edit our file:

class create_user_and_file {
    user { "puppet_test":
        ensure => present,
    }

    file { "fancy file":
        ensure => present,
        path => "/home/spike/Desktop/puppet_test_file",
        content => "Hello World",
        owner => "puppet_test",
        require => User["puppet_test"],
    }
}

node "rabbithole-local" {
    include create_user_and_file
}

When executing the file on rabbithole-local, our configuration will be applied. On any other system, nothing would happen. This mechanism is especially useful when using a puppetmaster setup in order to serve different configurations for each registered node.

Outlook

In the next post in this series, I’ll show you how to provision OS X systems using Puppet.