..

Intro to Puppet: The Bare Minimum

Last month, some of my coworkers were looking for a brief introduction to Puppet. Puppet is a type of configuration manager for your servers. It allows you to create definitions of your server that can then be automatically maintained. Puppet is mostly self documenting so it makes it easy to know what your servers are doing while giving you a great way to automate setting up large numbers of servers.

This is that brief talk. All code is available on Github in my puppet-walkthru repository. You will need Git, Virtual Box and Vagrant installed. To begin, clone the repository and launch the Vagrantfile:

git clone https://github.com/atomaka/puppet-walkthru.git
cd puppet-walkthru
vagrant up

This will setup a virtual machine on your computer with Puppet installed. All code can be found on the virtual machine in the /vagrant directory.

vagrant ssh
sudo su cd /vagrant

You are now ready to work through the first example.

1. Managing Users

Puppet maintains state on your computer using what are referred to as resources. The built-in resources provided by Puppet provide a good start. In example one, you can see how to use a Puppet resource to add and remove a user.

user { 'tm':
    ensure => present,
}
user { 'fowlks':
    ensure => absent,
}

You can run this code on your virtual machine with puppet apply manifests/1-user-type.pp. Afterward, you should notice that the user “tm” exists on your system.

The user resource type manages local users on your system. This works on a wide variety of systems, although some do not support some of the more specific features. In this example, we make sure the user “tm” is present on the system and make sure the user “fowlks” is not present.

ensure is a keyword for all Puppet resources. present and absent are the most common values although some resource types have others. ensure will make sure that definition exists on your server and absent will obviously do the opposite.

2. Managing Files

Managing files is one of the most common tasks for server administration and Puppet offers many ways to handle this. We’ll explore these in the next example.

file { '/tmp/test1.txt':
    ensure => present,
    content => 'Hello',
}
file { '/tmp/test2.txt':
    ensure => present,
    source => '/vagrant/files/test2.txt',
}
$something = "Hello"
file { '/tmp/test3.txt':
    ensure => present,
    content => template('/vagrant/templates/test3.txt.erb'),
}

Run this on your virtual machine using puppet apply manifests/2-file-type.pp and you should be alerted that three files were created. You can verify this by viewing the contents of the tmp directory with ls /tmp.

The first file resource simply creates a file at the specified location that says “Hello.” Unfortunately, this isn’t very useful since we do not want to have to type our entire cnfiguration file in our Puppet definition. The second resource is slightly more useful. This allows us to copy a file from our Puppet repository to a specified location.

Finally, we can also create templates. The last example uses a file from our repository and copies it to the specified location. However, we can also include variables that can be used in our file. In this case, we set a variable to something and it is then displayed in the file: You said: Hello. The contents of $something are used in the file.

3. Installing Packages

The last common task we’ll look at is installing packages. Puppet provides a way to define which packages can be installed. By default, this uses your distributions built-in package manager although there are ways to specify various providers. Our example shows the most basic usage.

package { 'vim':
    ensure => present,
}
package { 'alpine-pico':
    ensure => absent,
}

Try to open vim and you will notice it cannot run. Once you run this code with puppet apply manifests/3-package-type.pp, the vim package will then be present.

4. Ordering (or lack thereof)

The trickest thing for beginners to Puppet is dealing with its non-deterministic behavior. This is easier to show than explain.

notify { 'First': }
notify { 'Second': }
notify { 'Third': }
notify { 'Fourth': }
notify { 'Fifth': }
notify { 'Sixth': }
notify { 'Seventh': }

When run, you would expect this to spit out First, Second, …, Seventh in order. Invoke this code with puppet apply manifests/4-order-example.pp and be surprised at the results. The order of the code is much different than what is in the file. Furthermore, if you were to add notify { 'Eighth': } the ordering might change completely.

5. But I Need Order

But there are dependencies when setting up systems. Puppet allows for this, you just are required to explicitly define them. The biggest advantage here is that if one line of dependencies fails, your entire configuration does not. It takes some getting used to and can be frustrating, but it is worth it.

notify { 'First': }
notify { 'Second':
    require => Notify['First'],
}
notify { 'Third':
    require => Notify['Second'],
}
notify { 'Fourth':
    require => Notify['Third'],
}
notify { 'Fifth':
    require => Notify['Fourth'],
}
notify { 'Sixth':
    require => Notify['Fifth'],
}
notify { 'Seventh':
    require => Notify['Sixth'],
}

By using the require parameter, we have have forced ordering. If you run this code with puppet apply manifests/5-ordered-example.pp, you will see the order you expected in example number four.

6. Know Your Environment

Puppet also provides a way for you to know about the system that the Puppet code is running on with a system called Facter.

notify { "${::osfamily}": }
notify { "${::ipaddress}": }
notify { "${::uptime}": }

When run with puppet apply manifests/6-facts-example.pp, this code will display the information about the virtual machine you are running on. We will look at why this is useful later.

7. Doing Something Useful

Now that we have seen some quick forced examples of how to use Puppet, we now have enough knowledge to do something that is actually useful. Using Puppet, we can configure an entire service. If you are not familiar, NTP is a networking protocol for time management. It is useful for mainitaining the same system time across all of your servers. And we can use Puppet to install it!

package { 'ntp':
    ensure => present,
}
file { '/etc/ntp.conf':
    ensure => present,
    require => Package['ntp'],
    source => '/vagrant/files/ntp.conf.debian',
}
service { 'ntp':
    ensure => running,
    enable => true,
    subscribe => File['/etc/ntp.conf'],
}

When running this code with puppet apply manifest/7-full-example.pp, you should notice three things happen. First, the ntp package will be installed. Since we are on Ubuntu, this is done using apt-get. Secondly, a configuration file was copied from our Puppet repository to the location specified. Finally, the ntp service was started.

Install, configure, start is one of the most common patterns in Linux/UNIX systems administration and we can easily automate it with Puppet.

Something to note is our use of subscribe when using the service resource type. This makes sure that the ntp service is restarted only if the configuration file has changed.

7. Managing Multiple Operating Systems

Before this section, be sure to reset what we did in the previous example by running bash support/cleanup7.sh. We just need to uninstall ntp and our config file so we can do it all again.

Unfortunately, our environments are never uniform and we are stuck dealing with different versions of operating systems. Luckily, we have tools that we can use to deal with it. We touched on this in section six, but now we will actually use them to install ntp again. This time, our code will work on both Debian and RedHat family Linux distributions.

case $::osfamily {
    'RedHat': {
        $service = 'ntpd'
        $conf = 'ntp.conf.redhat'
    }
    'Debian': {
        $service = 'ntp'
        $conf = 'ntp.conf.debian'
    }
}
notify { 'OS Information':
    message => "${::osfamily}: Setting service to ${service} and conf to ${conf}",
    before => Package['ntp'],
}
package { 'ntp':
    ensure => present,
}
file { '/etc/ntp.conf':
    ensure => present,
    require => Package['ntp'],
    source => "/vagrant/files/${conf}",
}
service { $service:
    ensure => running,
    enable => true,
    subscribe => File['/etc/ntp.conf'],
}

When on our Ubuntu virtual machine, running this code with puppet apply manifest/8-independent-example.pp will setup NTP just as we did in example seven. However, this code can also run on RedHat / CentOS machines without any adjustments.

This is handled using the facts we discussed in section six. We check to see what distribution we are using with $::osfamily and make choices based on that. Since the service and the config file are different on RedHat, we assign variables to these and adjust them as needed.

You now have enough knowledge to get started with Puppet!