0% found this document useful (0 votes)
143 views51 pages

Learning Puppet

puppet doc

Uploaded by

rindra R
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
143 views51 pages

Learning Puppet

puppet doc

Uploaded by

rindra R
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 51

Learning Puppet

(Generated on December 29, 2011, from git revision


3426e97c3e7fbd308b63823630601d307c522dc8)
Learning Puppet
The web (including this site) is full of guides for how to solve specific problems with Puppet and
how to get Puppet running. This is something slightly different.

Latest: Defined Types →

Contents
Part one: Serverless Puppet

Resources and the RAL — Learn about the fundamental building blocks of system configuration.
Manifests — Start controlling your system by writing actual Puppet code.
Ordering — Learn about dependencies and refresh events, manage the relationships between
resources, and discover the fundamental Puppet design pattern.
Variables, Conditionals, and Facts — Make your manifests versatile by reading system
information.
Modules and Classes (Part One) — Start building your manifests into self-contained modules.
Templates — Use ERB to make your config files as flexible as your Puppet manifests.
Parameterized Classes (Modules, Part Two) — Learn how to pass parameters to classes and make
your modules more adaptable.
Defined Types — Model repeatable chunks of configuration by grouping basic resources into
super-resources.

Part two: Master/Agent Puppet

Coming soon!

Welcome
This is Learning Puppet. and itʼs part of the Puppet documentation. Specifically, itʼs the first part.

By which I donʼt mean itʼs about getting Puppet installed, or making sure your SSL certificates got
issued correctly; thatʼs the other first part. To be a little gnomic about it — because why not — this
series is less about how to use Puppet than it is about how to become a Puppet user. If youʼve heard
good things about Puppet but donʼt know where to start, this, hopefully, is it.

Itʼs a work in progress, and Iʼd love to read your feedback at [email protected].

Get Equipped
You canʼt make a knowledge omelette without breaking… stuff. Possibly eggs, maybe your systemʼs

Learning Puppet • Learning Puppet 2/51


entire configuration! Such is life.

So to learn Puppet effectively, you need a virtual machine you can experiment on fearlessly. And to
learn Puppet fast, you want a virtual machine with Puppet already installed, so you donʼt have to
learn to debug SSL problems before you know how to classify a node.

In short, you want this virtual machine:

Get the Learning Puppet VM

The root userʼs password is puppet, and for your convenience, the system is configured to write its
current IP address to the login screen about ten seconds after it boots.

If youʼd rather cook up your own VM than download one from the web, you can imitate it fairly
easily: this is a stripped-down CentOS 5.5 system with a hostname of “learn.puppet.demo,” Puppet
Enterprise installed using all default answers, iptables turned off, and the pe-puppet and pe-httpd
services stopped and disabled. (It also has Puppet language modes installed for Vim and Emacs, but
thatʼs not strictly necessary.)

To begin with, you wonʼt need separate agent and master VMs; youʼll be running Puppet in its
serverless mode on a single node. When we get to agent/master Puppet, weʼll walk through turning
on the puppet master and duplicating this system into a new agent node.

Compatibility Notes

The Learning Puppet VM is available in VMWare .vmx format and the cross-platform OVF format,
and has been tested with VMWare Fusion and VirtualBox.

Getting the VM working with VMWare is fairly simple, but some extra effort is necessary on
VirtualBox — the IP address it prints isnʼt externally reachable, and by default youʼll be unable to
SSH to the VM. You can enable SSH by turning on port forwarding, but since several examples (and
the eventual agent/master exercises) will require more network access than simply port 22, itʼs
wiser to configure two network interfaces.

METHOD 1

If youʼre on a network with working DHCP, you can simply expose the VM on that network and let it
obtain an IP address like any other computer:

Before starting the VM, modify its network settings to add a “Bridged Adapter” on adapter 2.
Once the VM is running, log in on its console and run ifconfig. Your host system should be able
to ping and SSH to the VM at its eth1 IP address.

METHOD 2

If you arenʼt connected to a network with working DHCP, or you donʼt want to expose the VM to
your local network (or take up one of its IP address slots), you can instead do the following:

Before starting the VM, modify its network settings to add “Host Only” network access on adapter
Learning Puppet • Learning Puppet 3/51
2.
Next, run ifconfig on your host system and confirm the subnet assigned to the vboxnet0
interface. (By default, this is 192.168.56.x, and your host systemʼs IP address is 192.168.56.1.)
Once the VM is running, log in on its console and run ifconfig eth1 192.168.56.2 (or some
other IP address within the relevant subnet); this should let you ping and SSH the box from your
host system, and you can add an entry to your host systemʼs /etc/hosts file to make things
more convenient.

Beyond this, teaching the use of virtualization software is outside the scope of this introduction, but
let me know if you run into trouble, and weʼll try to refine our approach over time.

Hit the Gas


And with that, youʼre ready to start.

Learning — Resources and the RAL


Resources are the building blocks of Puppet, and the division of resources into types and providers
is what gives Puppet its power.

You are at the beginning. — Index — Manifests →

Molecules
Imagine a systemʼs configuration as a collection of molecules; call them “resources.”

These pieces vary in size, complexity, and lifespan: a user account can be a resource, as can a
specific file, a software package, a running service, or a scheduled cron job. Even a single
invocation of a shell command can be a resource.

Any resource is very similar to a class of related things: every file has a path and an owner, and
every user has a name, a UID, and a group. Which is to say: similar resources can be grouped into
types. Furthermore, the most important attributes of a resource type are usually conceptually
identical across operating systems, regardless of how the implementations differ. That is, the
description of a resource can be abstracted away from its implementation.

These two insights form Puppetʼs resource abstraction layer (RAL). The RAL splits resources into
types (high-level models) and providers (platform-specific implementations), and lets you describe
resources in a way that can apply to any system.

Sync: Read, Check, Write


Puppet uses the RAL to both read and modify the state of resources on a system. Since itʼs a
Learning Puppet • Learning — Resources and the RAL 4/51
declarative system, Puppet starts with an understanding of what state a resource should have. To
sync the resource, it uses the RAL to query the current state, compares that against the desired
state, then uses the RAL again to make any necessary changes.

Anatomy of a Resource
In Puppet, every resource is an instance of a resource type and is identified by a title; it has a
number of attributes (which are defined by its type), and each attribute has a value.

The Puppet language represents a resource like this:

user { 'dave':
ensure => present,
uid => '507',
gid => 'admin',
shell => '/bin/zsh',
home => '/home/dave',
managehome => true,
}

This syntax is the heart of the Puppet language, and youʼll be seeing it a lot. Hopefully you can
already see how it lays out all of the resourceʼs parts (type, title, attributes, and values) in a fairly
straightforward way.

The Resource Shell


Puppet ships with a tool called puppet resource, which uses the RAL to let you query and modify
your system from the shell. Use it to get some experience with the RAL before learning to write and
apply manifests.

Puppet resourceʼs first argument is a resource type. If executed with no further arguments…

$ puppet resource user

… it will query the system and return every resource of that type it can recognize in the systemʼs
current state.

You can retrieve a specific resourceʼs state by providing a resource name as a second argument.

$ puppet resource user root

user { 'root':
home => '/var/root',
shell => '/bin/sh',
uid => '0',
ensure => 'present',
password => '*',

Learning Puppet • Learning — Resources and the RAL 5/51


gid => '0',
comment => 'System Administrator'
}

Note that puppet resource returns Puppet code when it reads a resource from the system! You can
use this code later to restore the resource to the state itʼs in now.

If any attribute=value pairs are provided as additional arguments to puppet resource, it will modify
the resource, which can include creating it or destroying it:

$ puppet resource user dave ensure=present shell="/bin/zsh" home="/home/dave"


managehome=true

notice: /User[dave]/ensure: created

user { 'dave':
ensure => 'present',
home => '/home/dave',
shell => '/bin/zsh'
}

(Note that this command line assignment syntax differs from the Puppet languageʼs normal
attribute => value syntax.)

Finally, if you specify a resource and use the --edit flag, you can change that resource in your text
editor; after the buffer is saved and closed, puppet resource will modify the resource to match your
changes.

The Core Resource Types


Puppet has a number of built-in types, and new native types can be distributed with modules.
Puppetʼs core types, the ones youʼll get familiar with first, are notify, file, package, service, exec,
cron, user, and group. Donʼt worry about memorizing them immediately, since weʼll be covering
various resources as we use them, but do take a second to print out a copy of the core types cheat
sheet, a double-sided page covering these eight types. It is doctor-recommended1 and has been
clinically shown to treat reference inflammation.

Documentation for all of the built-in types can always be found in the reference section of this site,
and can be generated on the fly with the puppet describe utility.

An Aside: puppet describe -s


You can get help for any of the Puppet executables by running them with the --help flag, but itʼs
worth pausing for an aside on puppet describeʼs -s flag.

$ puppet describe -s user

Learning Puppet • Learning — Resources and the RAL 6/51


user
====
Manage users. This type is mostly built to manage system
users, so it is lacking some features useful for managing normal
users.

This resource type uses the prescribed native tools for creating
groups and generally uses POSIX APIs for retrieving information
about them. It does not directly modify `/etc/passwd` or anything.

Parameters
----------
allowdupe, auth_membership, auths, comment, ensure, expiry, gid, groups,
home, key_membership, keys, managehome, membership, name, password,
password_max_age, password_min_age, profile_membership, profiles,
project, role_membership, roles, shell, uid

Providers
---------
directoryservice, hpuxuseradd, ldap, pw, user_role_add, useradd

-s makes puppet describe dump a compact list of the given resource typeʼs attributes and
providers. This isnʼt useful when learning about a type for the first time or looking up allowed
values, but itʼs fantastic when you have the name of an attribute on the tip of your tongue or you
canʼt remember which two out of “group,” “groups,” and “gid” are applicable for the user type.

Next
Puppet resource can be useful for one-off jobs, but Puppet was born for greater things. Time to
write some manifests.

1. The core types cheat sheet is not actually doctor-recommended. If youʼre a sysadmin with an M.D., please email me so I can
change this footnote.↩

Learning — Manifests
You understand the RAL; now learn about manifests and start writing and applying Puppet code.

← Resources and the RAL — Index — Resource Ordering →

No Strings Attached
You probably already know that Puppet usually runs in an agent/master (that is, client/server)
configuration, but ignore that for now. Itʼs not important yet and you can get a lot done without it,
so for the time being, we have no strings on us.

Learning Puppet • Learning — Manifests 7/51


Instead, weʼre going to use puppet apply, which applies a manifest on the local system. Itʼs the
simplest way to run Puppet, and it works like this:

$ puppet apply my_test_manifest.pp

Yeah, that easy.

You can use puppet — that is, without any subcommand — as a shortcut for puppet apply; it has
the rockstar parking in the UI because of how often it runs at an interactive command line. Iʼll
mostly be saying “puppet apply” for clarityʼs sake.

The behavior of Puppetʼs man pages is currently in flux. You can always get help for Puppetʼs
command line tools by running the tool with the --help flag; in the Learning Puppet VM, which uses
Puppet Enterprise, you can also run pe-man puppet apply to get the same help in a different
format. Versions of Puppet starting with the upcoming 2.7 will use Git-style man pages (man
puppet-apply) with improved formatting.

Manifests
Puppet programs are called “manifests,” and they use the .pp file extension.

The core of the Puppet language is the resource declaration, which represents the desired state of
one resource. Manifests can also use conditional statements, group resources into collections,
generate text with functions, reference code in other manifests, and do many other things, but it all
ultimately comes down to making sure the right resources are being managed the right way.

An Aside: Compilation
Manifests donʼt get used directly when Puppet syncs resources. Instead, the flow of a Puppet run
goes a little like this:

Learning Puppet • Learning — Manifests 8/51


Before being applied, manifests get compiled into a “catalog,” which is a directed acyclic graph that
only represents resources and the order in which they need to be synced. All of the conditional
logic, data lookup, variable interpolation, and resource grouping gets computed away during
compilation, and the catalog doesnʼt have any of it.

Why? Several really good reasons, which weʼll get to once we rediscover agent/master Puppet;1 itʼs
not urgent at the moment. But Iʼm mentioning it now as kind of an experiment: I think there are
several things in Puppet that are easy to explain if you understand that split and quite baffling if
you donʼt, so try keeping this in the back of your head and weʼll see if it pays off later.

OK, enough about that; letʼs write some code! This will all be happening on your main Learning
Puppet VM, so log in as root now; youʼll probably want to stash these test manifests somewhere
convenient, like /root/learning-manifests.

Resource Declarations
Letʼs start by just declaring a single resource:

# /root/training-manifests/1.file.pp

file {'testfile':
path => '/tmp/testfile',

Learning Puppet • Learning — Manifests 9/51


ensure => present,
mode => 0640,
content => "I'm a test file.",
}

And apply!

# puppet apply 1.file.pp


notice: /Stage[main]//File[testfile]/ensure: created
# cat /tmp/testfile
I'm a test file.
# ls -lah /tmp/testfile
-rw-r----- 1 root root 16 Feb 23 13:15 /tmp/testfile

Youʼve seen this syntax before, but letʼs take a closer look at the language here.

First, you have the type (“file”), followed by…


…a pair of curly braces that encloses everything else about the resource. Inside those, you
have…
…the resource title, followed by a colon…
…and then a set of attribute => value pairs describing the resource.

A few other notes about syntax:

Missing commas and colons are the number one syntax error made by learners. If you take out
the comma after ensure => present in the example above, youʼll get an error like this:

Could not parse for environment production: Syntax error at 'mode';


expected '}' at /root/manifests/1.file.pp:6 on node barn2.magpie.lan

Missing colons do about the same thing. So watch for that. Also, although you donʼt strictly need
the comma after the final attribute => value pair, you should always put it there anyhow. Trust
me.

Capitalization matters! You canʼt declare a resource with File {'testfile:'..., because that
does something entirely different. (Specifically, it breaks. But itʼs kind of similar to what we use to
tweak an existing resource, which weʼll get to later.)
Quoting values matters! Built-in values like present shouldnʼt be quoted, but normal strings
should be. For all intents and purposes, everything is a string, including numbers. Puppet uses
the same rules for single and double quotes as everyone else:
Single quotes are completely literal, except that you write a literal quote with \' and a literal
backslash with \\.

Double quotes let you interpolate $variables and add newlines with \n.

Whitespace is fungible for readability. Lining up the => arrows (sometimes called “fat commas”) is
Learning Puppet • Learning — Manifests 10/51
good practice if you ever expect someone else to read this code — note that future and mirror
universe versions of yourself count as “someone else.”

“
Exercise: Declare a file resource in a manifest and apply it! Try changing the
login message by setting the content of /etc/motd.

Once More, With Feeling!


Okay, you sort of have the idea by now. Letʼs make a whole wad of totally useless files! (And throw
in some notify resources for good measure.)

# /root/training-manifests/2.file.pp

file {'/tmp/test1':
ensure => present,
content => "Hi.",
}

file {'/tmp/test2':
ensure => directory,
mode => 0644,
}

file {'/tmp/test3':
ensure => link,
target => '/tmp/test1',
}

notify {"I'm notifying you.":} # Whitespace is fungible, remember.


notify {"So am I!":}

# puppet apply 2.file.pp


notice: /Stage[main]//File[/tmp/test2]/ensure: created
notice: /Stage[main]//File[/tmp/test3]/ensure: created
notice: /Stage[main]//File[/tmp/test1]/ensure: created
notice: I'm notifying you.
notice: /Stage[main]//Notify[I'm notifying you.]/message: defined 'message' as
'I'm notifying you.'
notice: So am I!
notice: /Stage[main]//Notify[So am I!]/message: defined 'message' as 'So am I!'

# ls -lah /tmp/test*
-rw-r--r-- 1 root root 3 Feb 23 15:54 test1
lrwxrwxrwx 1 root root 10 Feb 23 15:54 test3 -> /tmp/test1
-rw-r----- 1 root root 16 Feb 23 15:05 testfile

/tmp/test2:
total 16K
drwxr-xr-x 2 root root 4.0K Feb 23 16:02 .

Learning Puppet • Learning — Manifests 11/51


drwxrwxrwt 5 root root 4.0K Feb 23 16:02 ..

# cat /tmp/test3
Hi.

That was totally awesome. What just happened?

Titles and Namevars

All right, notice how we left out some important attributes there and everything still worked?
Almost every resource type has one attribute whose value defaults to the resourceʼs title. For the
file resource, thatʼs path; with notify, itʼs message. A lot of the time (user, group, package…), itʼs
plain old name.

To people who occasionally delve into the Puppet source code, the one attribute that defaults to the
title is called the “namevar,” which is a little weird but as good a name as any. Itʼs almost always the
attribute that amounts to the resourceʼs identity, the one thing that should always be unique about
each instance.

This can be a convenient shortcut, but be wary of overusing it; there are several common cases
where it makes more sense to give a resource a symbolic title and assign its name (-var) as a
normal attribute. In particular, itʼs a good idea to do so if a resourceʼs name is long or you want to
assign the name conditionally depending on the nature of the system.

notify {'bignotify':
message => "I'm completely enormous, and will mess up the formatting of
your
code! Also, since I need to fire before some other resource, you'll
need
to refer to me by title later using the Notify['title'] syntax, and
you
really don't want to have to type this all over again.",
}

The upshot is that our notify {"I'm notifying you.":} resource above has the exact same effect
as:

notify {'other title':


message => "I'm notifying you.",
}

… because the message attribute just steals the resource title if you donʼt give it anything of its own.

You canʼt declare the same resource twice: Puppet will always keep you from making resources with
duplicate titles, and will almost always keep you from making resources with duplicate
name/namevar values. (exec resources are the main exception.)

Learning Puppet • Learning — Manifests 12/51


And finally, you donʼt need an encyclopedic memory of what the namevar is for each resource —
when in doubt, just choose a descriptive title and specify the attributes you need.

644 = 755 For Directories

We said /tmp/test2/ should have permissions mode 0644, but our ls -lah showed mode 0755.
Thatʼs because Puppet groups the read bit and the traverse bit for directories, which is almost
always what you actually want. The idea is to let you recursively manage whole directories as mode
0644 without making all their files executable.

New Ensure Values

The file type has several different values for its ensure attribute: present, absent, file,
directory, and link. Theyʼre listed on the core types cheat sheet whenever you need to refresh
your memory, and theyʼre fairly self-explanatory.

The Destination
Hereʼs a pretty crucial part of learning to think like a Puppet user. Try applying that manifest again.

# puppet apply 2.file.pp


notice: I'm notifying you.
notice: /Stage[main]//Notify[I'm notifying you.]/message: defined 'message' as
'I'm notifying you.'
notice: So am I!
notice: /Stage[main]//Notify[So am I!]/message: defined 'message' as 'So am I!'

And again!

# rm /tmp/test3
# puppet apply 2.file.pp
notice: I'm notifying you.
notice: /Stage[main]//Notify[I'm notifying you.]/message: defined 'message' as
'I'm notifying you.'
notice: /Stage[main]//File[/tmp/test3]/ensure: created
notice: So am I!
notice: /Stage[main]//Notify[So am I!]/message: defined 'message' as 'So am I!'

The notifies are firing every time, because thatʼs what theyʼre for, but Puppet doesnʼt do anything
with the file resources unless theyʼre wrong on disk; if theyʼre wrong, it makes them right.
Remember how I said Puppet was declarative? This is how that pays off: You can apply the same
configuration every half hour without having to know anything about how the system currently
looks. Manifests describe the destination, and Puppet handles the journey.

Learning Puppet • Learning — Manifests 13/51


“
Exercise: Write and apply a manifest that’ll make sure Apache (httpd) is
running, use a web browser on your host OS to view the Apache welcome page,
then modify the manifest to turn Apache back off. (Hint: You’ll have to check the
cheat sheet or the types reference, because the service type’s ensure values
differ from the ones you’ve seen so far.)

“
Slightly more difficult exercise: Write and apply a manifest that uses the
ssh_authorized_key type to let you log into the learning VM as root without a
password. You’ll need to already have an SSH key.

Next
Resource declarations: Check! You know how to use the fundamental building blocks of Puppet
code, so now itʼs time to learn how those blocks fit together.

1. There are also a few I can mention now, actually. If you drastically refactor your manifest code and want to make sure it still
generates the same configurations, you can just intercept the catalogs and use a special diff tool on them; if the same nodes
get the same configurations, you can be sure the code acts the same without having to model the execution of the code in
your head. Compiling to a catalog also makes it much easier to simulate applying a configuration, and since the catalog is
just data, itʼs relatively easy to parse and analyze with your own tool of choice.↩

Learning — Resource Ordering


You understand manifests and resource declarations; now learn about metaparameters, resource
ordering, and one of the most useful patterns in Puppet.

← Manifests — Index — Variables →

Disorder
Letʼs look back on one of our manifests from the last page:

# /root/training-manifests/2.file.pp

file {'/tmp/test1':
ensure => present,
content => "Hi.",
}

file {'/tmp/test2':
ensure => directory,

Learning Puppet • Learning — Resource Ordering 14/51


mode => 644,
}

file {'/tmp/test3':
ensure => link,
target => '/tmp/test1',
}

notify {"I'm notifying you.":}


notify {"So am I!":}

Although we wrote these declarations one after another, Puppet might sync them in any order:
unlike with a procedural language, the physical order of resources in a manifest doesnʼt imply a
logical order.

But some resources depend on other resources. So how do we tell Puppet which ones go first?

Metaparameters, Resource References, and Ordering


file {'/tmp/test1':
ensure => present,
content => "Hi.",
}

notify {'/tmp/test1 has already been synced.':


require => File['/tmp/test1'],
}

Each resource type has its own set of attributes, but thereʼs another set of attributes, called
metaparameters, which can be used on any resource. (Theyʼre meta because they donʼt describe
any feature of the resource that you could observe on the system after Puppet finishes; they only
describe how Puppet should act.)

There are four metaparameters that let you arrange resources in order: before, require, notify,

and subscribe. All of them accept a resource reference (or an array1 of them). Resource references
look like this:

Type['title']

(Note the square brackets and capitalized resource type!)

AN ASIDE: CAPITALIZATION

The easy way to remember this is that you only use the lowercase type name when declaring a new
resource. Any other situation will always call for a capitalized type name.

This will get more important in another couple lessons, so Iʼll mention it again later.

Learning Puppet • Learning — Resource Ordering 15/51


Before and Require

before and require make simple dependency relationships, where one resource must be synced
before another. before is used in the earlier resource, and lists resources that depend on it;
require is used in the later resource and lists the resources that it depends on.

These two metaparameters are just different ways of writing the same relationship — our example
above could just as easily be written like this:

file {'/tmp/test1':
ensure => present,
content => "Hi.",
before => Notify['/tmp/test1 has already been synced.'],
# (See what I meant about symbolic titles being a good idea?)
}

notify {'/tmp/test1 has already been synced.':}

Notify and Subscribe

A few resource types2 can be “refreshed” — that is, told to react to changes in their environment.
For a service, this usually means restarting when a config file has been changed; for an exec
resource, this could mean running its payload if any user accounts have been changed. (Note that
refreshes are performed by Puppet, so they only occur during Puppet runs.)

The notify and subscribe metaparameters make dependency relationships the way before and
require do, but they also make refresh relationships. Not only will the earlier resource in the pair
get synced first, but if Puppet makes any changes to that resource, it will send a refresh event to the
later resource, which will react accordingly.

Chaining

file {'/tmp/test1':
ensure => present,
content => "Hi.",
}

notify {'after':
message => '/tmp/test1 has already been synced.',
}

File['/tmp/test1'] -> Notify['after']

Thereʼs one last way to declare relationships: chain resource references with the ordering (->) and
notification (~>; note the tilde) arrows. The arrows can point in either direction (<- works too), and
you should think of them as representing the flow of time: the resource at the blunt end of the
arrow will be synced before the resource the arrow points at.

Learning Puppet • Learning — Resource Ordering 16/51


The example above yields the same dependency as the two examples before it. The benefit of this
alternate syntax may not be obvious when weʼre working with simple examples, but it can be much
more expressive and legible when weʼre working with resource collections.

Autorequire

Some of Puppetʼs resource types will notice when an instance is related to other resources, and
theyʼll set up automatic dependencies. The one youʼll use most often is between files and their
parent directories: if a given file and its parent directory are both being managed as resources,
Puppet will make sure to sync the parent directory before the file.

Donʼt sweat much about the details of autorequiring; itʼs fairly conservative and should generally do
the right thing without getting in your way. If you forget itʼs there and make explicit dependencies,
your code will still work.

Summary
So to sum up: whenever a resource depends on another resource, use the before or require
metaparameter or chain the resources with ->. Whenever a resource needs to refresh when another
resource changes, use the notify or subscribe metaparameter or chain the resources with ~>.
Some resources will autorequire other resources if they see them, which can save you some effort.

Hopefully thatʼs all pretty clear! But even if it is, itʼs rather abstract — making sure a notify fires after
a file is something of a “hello world” use case, and not very illustrative. Letʼs break something!

Example: sshd
Youʼve probably been using SSH and your favorite terminal app to interact with the Learning Puppet
VM, so letʼs go straight for the most-annoying-case scenario: weʼll pretend someone accidentally
gave the wrong person (i.e., us) sudo privileges, and have you ruin rootʼs ability to SSH to this box.
Weʼll use Puppet to bust it and Puppet to fix it.

First, if you got the ssh_authorized_key exercise from the last page working, undo it.

# mv ~/.ssh/authorized_keys ~/old_ssh_authorized_keys

Now letʼs get a copy of the current sshd config file; going forward, weʼll use our new copy as the
canonical source for that file.

# cp /etc/ssh/sshd_config ~/learning-manifests/

Next, edit our new copy of the file. Thereʼs a line in there that says PasswordAuthentication yes;
find it, and change the yes to a no. Then start writing some Puppet!

Learning Puppet • Learning — Resource Ordering 17/51


# /root/learning-manifests/break_ssh.pp
file { '/etc/ssh/sshd_config':
ensure => file,
mode => 600,
source => '/root/learning-manifests/sshd_config',
# And yes, that's the first time we've seen the "source" attribute.
# It accepts absolute paths and puppet:/// URLs, about which more later.
}

Except that wonʼt work! (Donʼt run it, and if you did, read this footnote.3) If we apply this manifest,
the config file will change, but sshd will keep acting on the old config file until it restarts… and if itʼs
only restarting when the system reboots, that could be years from now.

If we want the service to change its behavior as soon as we change our policy, weʼll have to tell it to
monitor the config file.

# /root/learning-manifests/break_ssh.pp, again
file { '/etc/ssh/sshd_config':
ensure => file,
mode => 600,
source => '/root/learning-manifests/sshd_config',
}

service { 'sshd':
ensure => running,
enable => true,
hasrestart => true,
hasstatus => true,
# FYI, those last two attributes default to false, since
# bad init scripts are more or less endemic.
subscribe => File['/etc/ssh/sshd_config'],
}

And thatʼll do it! Run that manifest with puppet apply, and after you log out, you wonʼt be able to
SSH into the VM again. Victory.

To fix it, youʼll have to log into the machine directly — use the screen provided by your
virtualization app. Once youʼre there, youʼll just have to edit /root/learning-
manifests/sshd_config again to change the PasswordAuthentication setting and re-apply the
same manifest; Puppet will replace /etc/ssh/sshd_config with the new version, restart the service,
and re-enable remote password logins. (And you can put your SSH key back now, if you like.)

Package/File/Service
The example we just saw was very close to a pattern youʼll see constantly in production Puppet
code, but it was missing a piece. Letʼs complete it:

Learning Puppet • Learning — Resource Ordering 18/51


# /root/learning-manifests/break_ssh.pp
package { 'openssh-server':
ensure => present,
before => File['/etc/ssh/sshd_config'],
}

file { '/etc/ssh/sshd_config':
ensure => file,
mode => 600,
source => '/root/learning-manifests/sshd_config',
}

service { 'sshd':
ensure => running,
enable => true,
hasrestart => true,
hasstatus => true,
subscribe => File['/etc/ssh/sshd_config'],
}

This is package/file/service, one of the most useful patterns in Puppet: the package resource makes
sure the software is installed, the config file depends on the package resource, and the service
subscribes to changes in the config file.

Itʼs hard to understate the importance of this pattern; if this was all you knew how to do with
Puppet, you could still do a fair amount of work. But weʼre not done yet.

Next
Now that you can sync resources in their proper order, itʼs time to make your manifests aware of
the outside world with variables, facts, and conditionals.

1. Arrays in Puppet are made with square brackets and commas, so an array of resource references would [ Notify['look'],
Notify['like'], Notify['this'] ].↩

2. Of the built-in types, only exec, service, and mount can be refreshed.↩

3. If you DID apply the incomplete manifest, something interesting happened: your machine is now in a half-rolled-out
condition that puts the lie to what I said earlier about not having to worry about the systemʼs current state. Since the config
file is now in sync with its desired state, Puppet wonʼt change it during the next run, which means applying the complete
manifest wonʼt cause the service to refresh until either the source file or the file on the system changes one more time.

In practice, this isnʼt a huge problem, because only your development machines are likely to end up in this state; your
production nodes wonʼt have been given incomplete configurations. In the meantime, you have two options for cleaning up
after applying an incomplete manifest: For a one-time fix, echo a bogus comment to the bottom of the file on the system
(echo "# ignoreme" >> /etc/ssh/sshd_config), or for a more complete approach, make a comment in the source file that
contains a version string, which you can update whenever you make significant changes to the associated manifest(s). Both
of these approaches will mark the config file as out of sync, replace it during the Puppet run, and send the refresh event to
the service.↩

Learning — Variables, Conditionals, and


Learning Puppet • Learning — Variables, Conditionals, and Facts 19/51
Learning — Variables, Conditionals, and
Facts
You can write manifests and order resources; now, add logic and flexibility with conditional
statements and variables.

← Ordering — Index — Modules (Part One) →

Variables
Variables! Iʼm going to bet you pretty much know this drill, so letʼs move a little faster:

$variables always start with a dollar sign. You assign to variables with the = operator.

Variables can hold strings, numbers, special values (false, undef…), arrays, and hashes.
If youʼve never assigned a variable, you can actually still use it — its value will be undef. (You can
also explicitly assign undef as a value, although the use case for that is somewhat advanced.)

You can use variables as the value for any resource attribute, or as the title of a resource.
You can also interpolate variables inside strings, if you use double-quotes. To distinguish a
${variable} from the surrounding text, you should wrap its name in curly braces.

Every variable has a short local name and a long fully-qualified name. Fully qualified variables
look like $scope::variable. Top scope variables are the same, but their scope is nameless. (For
example: $::top_scope_variable.)

If you reference a variable with its short name and it isnʼt present in the local scope, Puppet will
also check the top scope;1 this means you can effectively leave off the leading :: on top scope
variables and treat them as globals.

You can only assign the same variable once in a given scope.2

$longthing = "Imagine I have something really long in here. Like an SSH


key, let's say."

file {'authorized_keys':
path => '/root/.ssh/authorized_keys',
content => $longthing,
}

Pretty easy.

Facts
And now, a teaspoon of magic.

Before you even start writing your manifests, Puppet builds you a stash of pre-assigned variables.
Check it out:

Learning Puppet • Learning — Variables, Conditionals, and Facts 20/51


# hosts-simple.pp

# Host type reference:
# http://docs.puppetlabs.com/references/stable/type.html#host

host {'self':
ensure => present,
name => $::fqdn,
host_aliases => ['puppet', $::hostname],
ip => $::ipaddress,
}

file {'motd':
ensure => file,
path => '/etc/motd',
mode => 0644,
content => "Welcome to ${::hostname},\na ${::operatingsystem} island in
the sea of ${::domain}.\n",
}

# puppet apply hosts-simple.pp

notice: /Stage[main]//Host[puppet]/ensure: created


notice: /Stage[main]//File[motd]/ensure: defined content as
'{md5}d149026e4b6d747ddd3a8157a8c37679'

# cat /etc/hosts
# HEADER: This file was autogenerated at Mon Apr 25 14:39:11 -0700 2011
# HEADER: by puppet. While it can still be managed manually, it
# HEADER: is definitely not recommended.
# Do not remove the following line, or various programs
# that require network functionality will fail.
127.0.0.1 localhost.localdomain localhost
::1 localhost6.localdomain6 localhost6
172.16.158.137 learn.puppet.demo puppet learn

Our manifests are starting to get versatile, with pretty much no real work on our part.

Hostname? IPaddress?

So where did those helpful variables come from? Theyʼre “facts.” Puppet ships with a tool called
Facter, which ferrets out your system information, normalizes it into a set of variables, and passes
them off to Puppet. The compiler then has access to those facts when itʼs reading a manifest.

There are a lot of different facts, and the easiest way to get a list of them is to simply run facter at
your VMʼs command line. Youʼll get back a long list of key/value pairs separated by the familiar =>
hash rocket. To use one of these facts in your manifests, just prepend a dollar sign to its name
(along with a ::, because being explicit about namespaces is a good habit).

Most kinds of system will have at least a few facts that arenʼt available on other kinds of system

Learning Puppet • Learning — Variables, Conditionals, and Facts 21/51


(e.g., try comparing Facterʼs output on your CentOS VM to what it does on an OS X machine), and it
can get fuzzier if youʼre extending Facter with custom facts, but thereʼs a general core of facts that
give you the same info everywhere. Youʼll get a feel for them pretty quickly, and can probably guess
most of them just by reading the list of names.

Conditional Statements
Puppet has a fairly complete complement of conditional syntaxes, and the info available in facts
makes it really easy to code different behavior for different systems.

If

Weʼll start with your basic if statement. Same as it ever was:

if condition {
block of code
}
elsif condition {
block of code
}
else {
block of code
}

The else and any number of elsif statements are optional.

if $is_virtual == 'true' {
service {'ntpd':
ensure => stopped,
enable => false,
}
}
else {
service { 'ntpd':
name => 'ntpd',
ensure => running,
enable => true,
hasrestart => true,
require => Package['ntp'],
}
}

The blocks of code for each condition can contain any Puppet code.

WHAT IS FALSE?

The Puppet languageʼs data types are kind of loose, and a lot of things tend to get represented
internally as strings, so itʼs worth calling out what exactly will be treated as false by an if
statement:

Learning Puppet • Learning — Variables, Conditionals, and Facts 22/51


undef (the value of an unassigned variable)

'' (the empty string)

false

Any expression that evaluates to false.

In particular, be aware that the numeral 0 and the string “false” are both true. This means that while
you can use some variables alone as an if-condition, you canʼt use facts that way. Facts are always
read into Puppet as strings, so you need to test conceptually-boolean facts for their string value
instead of their literal truth or falsehood.

CONDITIONS

Conditions can get pretty sophisticated: you can use any valid expression in an if statement.
Usually, this is going to mean using one of the standard comparison operators (==, !=, <, >, <=, >=),
the regex match operators (=~ and !~), or the in operator (which tests whether the right operand
contains the left one).

Case

Also probably familiar: the case statement. (Or switch, or whatever your language of choice calls it.)

case $operatingsystem {
centos: { $apache = "httpd" }
# Note that these matches are case-insensitive.
redhat: { $apache = "httpd" }
debian: { $apache = "apache2" }
ubuntu: { $apache = "apache2" }
default: { fail("Unrecognized operating system for webserver") }
# "fail" is a function. We'll get to those later.
}
package {'apache':
name => $apache,
ensure => latest,
}

Instead of testing a condition up front, case matches a variable against a bunch of possible values.
default is a special value, which does exactly what it sounds like.

CASE MATCHING

Matches can be simple strings (like above), regular expressions, or comma-separated lists of either.

String matching is case-insensitive, like the == comparison operator. Regular expressions are
denoted with the slash-quoting used by Perl and Ruby; theyʼre case-sensitive by default, but you
can use the (?i) and (?-i) switches to turn case-insensitivity on and off inside the pattern. Regex
matches also assign captured subpatterns to $1, $2, etc. inside the associated code block, with $0
containing the whole matching string.

Learning Puppet • Learning — Variables, Conditionals, and Facts 23/51


Hereʼs a regex example:

case $ipaddress_eth0 {
/^127[\d.]+$/: {
notify {'misconfig':
message => "Possible network misconfiguration: IP address of $0",
}
}
}

And hereʼs the example from above, rewritten and more readable:

case $operatingsystem {
centos, redhat: { $apache = "httpd" }
debian, ubuntu: { $apache = "apache2" }
default: { fail("Unrecognized operating system for webserver") }
}

Selectors

Selectors might be less familiar; theyʼre kind of like the ternary operator, and kind of like the case
statement.

Instead of choosing between a set of code blocks, selectors choose between a group of possible
values. You canʼt use them on their own; instead, theyʼre usually used to assign a variable.

$apache = $operatingsystem ? {
centos => 'httpd',
redhat => 'httpd',
/(?i)(ubuntu|debian)/ => "apache2-$1",
# (Don't actually use that package name.)
default => undef,
}

Careful of the syntax, there: it looks kind of like weʼre saying $apache = $operatingsystem, but
weʼre not. The question mark flags $operatingsystem as the pivot of a selector, and the actual
value that gets assigned is determined by which option $operatingsystem matches. Also note how
the syntax differs from the case syntax: it uses hash rockets and line-end commas instead of colons
and blocks, and you canʼt use lists of values in a match. (If you want to match against a list, you
have to fake it with a regular expression.)

It can look a little awkward, but there are plenty of situations where itʼs the most concise way to get
a value sorted out; if youʼre ever not comfortable with it, you can just use a case statement to assign
the variable instead.

Selectors can also be used directly as values for a resource attribute, but try not to do that, because
it gets ugly fast.
Learning Puppet • Learning — Variables, Conditionals, and Facts 24/51
Exercises

“
Exercise: Use the $operatingsystem fact to write a manifest that installs a build
environment on Debian-based (“debian” and “ubuntu”) and Enterprise Linux-
based (“centos,” “redhat”) machines. (Both types of system require the gcc
package, but Debian-type systems also require build-essential.)

“
Exercise: Write a manifest that installs and configures NTP for Debian-based
and Enterprise Linux-based Linux systems. This will be a package/file/service
pattern where you’ll be shipping different config files (Debian version, Red Hat
version — remember the file type’s “source” attribute) and using different
service names (ntp and ntpd, respectively).

(Use a second manifest to disable the NTP service after you’ve gotten this
example working; NTP can behave kind of uselessly in a virtual machine.)

Next
Now that your manifests can adapt to different kinds of systems, itʼs time to start grouping
resources and conditionals into meaningful units. Onward to classes, defined resource types, and
modules!

1. Itʼs actually a little more complicated than that, but donʼt worry about it for now. You can read up on it later.↩

2. This has to do with the declarative nature of the Puppet language: the idea is that the order in which you read the file
shouldnʼt matter, so changing a value halfway through is illegal, since it would make the results order-dependent.

In practice, this isnʼt the full story, because you canʼt currently read a variable from anywhere north of its assignment. Weʼre
working on that.↩

Learning — Modules and Classes (Part One)


You can write some pretty sophisticated manifests at this point, but theyʼre still at a fairly low
altitude, going resource-by-resource-by-resource. Now, zoom out with resource collections.

← Variables, etc. — Index — Templates →

Collecting and Reusing


Learning Puppet • Learning — Modules and Classes (Part One) 25/51
At some point, youʼre going to have Puppet code that fits into a couple of different buckets: really
general stuff that applies to all your machines, more specialized stuff that only applies to certain
classes of machines, and very specific stuff thatʼs meant for a few nodes at most.

So… you could just paste in all your more general code as boilerplate atop your more specific code.
There are ways to do that and get away with it. But thatʼs the road down into the valley of the
4,000-line manifest. Better to separate your code out into meaningful units and then call those
units by name as needed.

Thus, resource collections and modules! In a few minutes, youʼll be able to maintain your manifest
code in one place and declare whole groups of it like this:

class {'security_base': }
class {'webserver_base': }
class {'appserver': }

And after that, itʼll get even better. But first things first.

Classes
Classes are singleton collections of resources that Puppet can apply as a unit. You can think of
them as blocks of code that can be turned on or off.

If you know any object-oriented programming, try to ignore it for a little while, because thatʼs not
the kind of class weʼre talking about. Puppet classes could also be called “roles” or “aspects;” they
describe one part of what makes up a systemʼs identity.

Defining

Before you can use a class, you have to define it, which is done with the class keyword, a name,
and a block of code:

class someclass {
...
}

Well, hey: you have a block of code hanging around from last chapterʼs exercises, right? Chuck it in!

# ntp-class1.pp

class ntp {
case $operatingsystem {
centos, redhat: {
$service_name = 'ntpd'
$conf_file = 'ntp.conf.el'
}
debian, ubuntu: {

Learning Puppet • Learning — Modules and Classes (Part One) 26/51


$service_name = 'ntp'
$conf_file = 'ntp.conf.debian'
}
}

package { 'ntp':
ensure => installed,
}

service { 'ntp':
name => $service_name,
ensure => running,
enable => true,
subscribe => File['ntp.conf'],
}

file { 'ntp.conf':
path => '/etc/ntp.conf',
ensure => file,
require => Package['ntp'],
source => "/root/learning-manifests/${conf_file}",
}
}

Go ahead and apply that. In the meantime:

AN ASIDE: NAMES, NAMESPACES, AND SCOPE

Class names have to start with a lowercase letter, and can contain lowercase alphanumeric
characters and underscores. (Just your standard slightly conservative set of allowed characters.)

Class names can also use a double colon (::) as a namespace separator. (Yes, this should look
familiar.) This is a good way to show which classes are related to each other; for example, you can
tell right away that somethingʼs going on between apache::ssl and apache::vhost. This will
become more important about two feet south of here.

Also, class definitions introduce new variable scopes. That means any variables you assign within
wonʼt be accessible by their short names outside the class; to get at them from elsewhere, you
would have to use the fully-qualified name (e.g. $apache::ssl::certificate_expiration). It also
means you can localize — mask — variable short names in use outside the class; if you assign a
$fqdn variable in a class, you would get the new value instead of the value of the Facter-supplied
variable, unless you used the fully-qualified fact name ($::fqdn).

Declaring

Okay, back to our example, which youʼll have noticed by now doesnʼt actually do anything.

# puppet apply ntp-class1.pp


(...silence)

Learning Puppet • Learning — Modules and Classes (Part One) 27/51


The code inside the class was properly parsed, but the compiler didnʼt build any of it into the
catalog, so none of the resources got synced. For that to happen, the class has to be declared.

You actually already know the syntax to do that. A class definition just enables a unique instance of
the class resource type; once itʼs defined, you can declare it like any other resource:

# ntp-class1.pp

class ntp {
case $operatingsystem {
centos, redhat: {
$service_name = 'ntpd'
$conf_file = 'ntp.conf.el'
}
debian, ubuntu: {
$service_name = 'ntp'
$conf_file = 'ntp.conf.debian'
}
}

package { 'ntp':
ensure => installed,
}

service { 'ntp':
name => $service_name,
ensure => running,
enable => true,
subscribe => File['ntp.conf'],
}

file { 'ntp.conf':
path => '/etc/ntp.conf',
ensure => file,
require => Package['ntp'],
source => "/root/learning-manifests/${conf_file}",
}
}

# Then, declare it:
class {'ntp': }

This time, all those resources will end up in the catalog:

# puppet apply --verbose ntp-class1.pp

info: Applying configuration version '1305066883'


info: FileBucket adding /etc/ntp.conf as {md5}5baec8bdbf90f877a05f88ba99e63685
info: /Stage[main]/Ntp/File[ntp.conf]: Filebucketed /etc/ntp.conf to puppet
with sum 5baec8bdbf90f877a05f88ba99e63685
notice: /Stage[main]/Ntp/File[ntp.conf]/content: content changed
'{md5}5baec8bdbf90f877a05f88ba99e63685' to
'{md5}dc20e83b436a358997041a4d8282c1b8'

Learning Puppet • Learning — Modules and Classes (Part One) 28/51


info: /Stage[main]/Ntp/File[ntp.conf]: Scheduling refresh of Service[ntp]
notice: /Stage[main]/Ntp/Service[ntp]/ensure: ensure changed 'stopped' to
'running'
notice: /Stage[main]/Ntp/Service[ntp]: Triggered 'refresh' from 1 events

Defining the class makes it available; declaring activates it.

INCLUDE

Thereʼs another way to declare classes, but it behaves a little bit differently:

include ntp
include ntp
include ntp

The include function will declare a class if it hasnʼt already been declared, and will do nothing if it
has. This means you can safely use it multiple times, whereas the resource syntax can only be used
once. The drawback is that include canʼt currently be used with parameterized classes, on which
more later.

So which should you choose? Neither, yet: learn to use both, and decide later, after weʼve covered
site design and parameterized classes.

Classes In Situ

Youʼve probably already guessed that classes arenʼt enough: even with the code above, youʼd still
have to paste the ntp definition into all your other manifests. So itʼs time to meet the module
autoloader!

AN ASIDE: PRINTING CONFIG

But first, weʼll need to meet its friend, the modulepath.

# puppet apply --configprint modulepath


/etc/puppetlabs/puppet/modules

By the way, --configprint is wonderful. Puppet has a lot of config options, all of which have
default values and site-specific overrides in puppet.conf, and trying to memorize them all is a pain.
You can use --configprint on most of the Puppet tools, and theyʼll print a value (or a bunch, if you
use --configprint all) and exit.

(As for the modulepath, it looks like a single directory at the moment but is actually a colon-
separated1 list of directories.)

Modules
Modules are re-usable bundles of code and data. Puppet autoloads manifests from the modules in

Learning Puppet • Learning — Modules and Classes (Part One) 29/51


its modulepath, which means you can declare a class stored in a module from anywhere. Letʼs just
convert that last class to a module immediately, so you can see what Iʼm talking about:

# cd /etc/puppetlabs/puppet/modules
# mkdir ntp; cd ntp; mkdir manifests; cd manifests
# vim init.pp

# init.pp

class ntp {
case $operatingsystem {
centos, redhat: {
$service_name = 'ntpd'
$conf_file = 'ntp.conf.el'
}
debian, ubuntu: {
$service_name = 'ntp'
$conf_file = 'ntp.conf.debian'
}
}

package { 'ntp':
ensure => installed,
}

service { 'ntp':
name => $service_name,
ensure => running,
enable => true,
subscribe => File['ntp.conf'],
}

file { 'ntp.conf':
path => '/etc/ntp.conf',
ensure => file,
require => Package['ntp'],
source => "/root/learning-manifests/${conf_file}",
}
}

# (Remember not to declare the class yet.)

And now, the reveal:2

# cd ~
# puppet apply -e "include ntp"

Which works! You can now include the class from any manifest, without having to cut and paste
anything.

Learning Puppet • Learning — Modules and Classes (Part One) 30/51


But weʼre not quite done yet. See how the manifest is referring to some files stored outside the
module? Letʼs fix that:

# mkdir /etc/puppetlabs/puppet/modules/ntp/files
# mv /root/learning-manifests/ntp.conf.*
/etc/puppetlabs/puppet/modules/ntp/files/
# vim /etc/puppetlabs/puppet/modules/ntp/manifests/init.pp

# ...
file { 'ntp.conf':
path => '/etc/ntp.conf',
ensure => file,
require => Package['ntp'],
# source => "/root/learning-manifests/${conf_file}",
source => "puppet:///modules/ntp/${conf_file}",
}
}

There — our little example from last chapter has grown up into a self-contained blob of awesome.

Obtaining Modules

Puppet Labs provides the Puppet Forge, the place to share and find Puppet modules. The Puppet
Forge is a great place to start looking for modules that you can use or adapt for your environment.
Most of these modules are open source and you can easily contribute updates and changes to
improve or enhance these modules. You can also contribute your own modules.

Module Structure
A module is just a directory with stuff in it, and the magic comes from putting that stuff where
Puppet expects to find it. Which is to say, arranging the contents like this:

{module}/
files/
lib/
manifests/
init.pp
{class}.pp
{defined type}.pp
{namespace}/
{class}.pp
{class}.pp

templates/
tests/

Learning Puppet • Learning — Modules and Classes (Part One) 31/51


The main directory should be named after the module. All of the manifests go in the manifests
directory. Each manifest contains only one class (or defined type). Thereʼs a special manifest called
init.pp that holds the moduleʼs main class, which should have the same name as the module.
Thatʼs your barest-bones module: main folder, manifests folder, init.pp, just like we used in the ntp
module above.

But if that was all a module was, itʼd make more sense to just load your classes from one flat folder.
Modules really come into their own with namespacing and grouping of classes.

Manifests, Namespacing, and Autoloading

The manifests directory can hold any number of other classes and even folders of classes, and
Puppet uses namespacing to find them. Say we have a manifests folder that looks like this:

foo/
manifests/
init.pp
bar.pp
bar/
baz.pp

The init.pp file should contain class foo { ... }, bar.pp should contain class foo::bar { ...
}, and baz.pp should contain class foo::bar::baz { ... }.

This can be a little disorienting at first, but I promise youʼll get used to it. Basically, init.pp is special,
and all of the other classes (each in its own manifest) should be under the main classʼs namespace.
If you add more levels of directory hierarchy, they get interpreted as more levels of namespace
hierarchy. This lets you group related classes together, or split the implementation of a complex
resource collection out into conceptually separate bits.

Files

Puppet can serve files from modules, and it works identically regardless of whether youʼre doing
serverless or agent/master Puppet. Everything in the files directory in the ntp module is available
under the puppet:///modules/ntp/ URL. Likewise, a test.txt file in the testing moduleʼs files
could be retrieved as puppet:///modules/testing/test.txt.

Tests

Once you start writing modules you plan to keep for more than a day or two, read our brief guide to
module smoke testing. Itʼs pretty simple, and will eventually pay off.

Templates

More on templates later.

Learning Puppet • Learning — Modules and Classes (Part One) 32/51


Lib

Puppet modules can also serve executable Ruby code from their lib directories, to extend Puppet
and Facter. (Remember how I mentioned extending Facter with custom facts? This is where they
live.) Itʼll be a while before we cover any of that.

Module Scaffolding

Since youʼll be dealing with those same five subdirectories so much, consider adding a function for
them to your ~/.bashrc file.

mkmod() {
mkdir "$1"
mkdir "$1/files" "$1/lib" "$1/manifests" "$1/templates" "$1/tests"
}

Exercises

“
Exercise: Build an Apache2 module and class, which ensures Apache is
installed and running and manages its config file. While you’re at it, make
Puppet manage the DocumentRoot and put a custom 404 page and default
index.html in place.

Set any files or package/service names that might vary per distro conditionally,
failing if we’re not on CentOS; this’ll let you cleanly shim in support for other
distros once you need it.

We’ll be using this module some more in future lessons.

Next
So whatʼs with those static config files weʼre shipping around? If our classes can do different things
on different systems, shouldnʼt our ntp.conf and httpd.conf files be able to do the same? Yes. Yes
they should.

1. Well, system path separator-separated. On POSIX systems, thatʼs a colon; on Windows, itʼs a semicolon. ↩

2. The -e flag lets you give puppet apply a line of manifest code instead of a file, same as with Perl or Ruby.↩

Learning — Templates
File serving isnʼt the be-all/end-all of getting content into place. Before we get to parameterized
classes and defined types, take a break to learn about templates, which let you make your config
Learning Puppet • Learning — Templates 33/51
files as versatile as your manifests.

← Modules (part one) — Index — Modules (part two) →

Templating
Okay: in the last chapter, you built a module that shipped and managed a configuration file, which
was pretty cool. And if you expect all your enterprise Linux systems to use the exact same set of
NTP servers, thatʼs probably good enough. Except letʼs say you decide most of your machines
should use internal NTP servers — whose ntpd configurations are also managed by Puppet, and
which should be asking for the time from an external source. The number of files youʼll need to
ship just multiplied, and theyʼll only differ by three or four lines, which seems redundant and
wasteful.

It would be much better if you could use all the tricks you learned in Variables, Conditionals, and
Facts to rummage around in the actual text of your configuration files. Thus: templates!

Puppet can use ERB templates anywhere a string is called for. (Like a fileʼs content attribute, for
instance, or the value of a variable.) Templates go in the (wait for it) templates/ directory of a
module, and will mostly look like normal configuration files (or what-have-you), except for the
occasional <% tag with Ruby code %>.

Yes, Ruby — unfortunately you canʼt use the Puppet language in templates. But usually youʼll only
be printing a variable or doing a simple loop, which youʼll get a feel for almost instantly. Anyway,
letʼs cut to the chase:

Some Simple ERB


First, keep in mind that facts, global variables, and variables defined in the current scope are
available to a template as standard Ruby local variables, which are plain words without a $ sigil in
front of them. Variables from other scopes are reachable, but to read them, you have to call the
lookupvar method on the scope object. (For example, scope.lookupvar('apache::user').)

Tags

ERB tags are delimited by angle brackets with percent signs just inside. (There isnʼt any HTML-like
concept of opening or closing tags.)

<% document = "" %>

Tags contain one or more lines of Ruby code, which can set variables, munge data, implement
control flow, or… actually, pretty much anything, except for print text in the rendered output.

Printing an Expression
Learning Puppet • Learning — Templates 34/51
Printing an Expression

For that, you need to use a printing tag, which looks like a normal tag with an equals sign right
after the opening delimiter:

<%= sectionheader %>


environment = <%= gitrevision[0,5] %>

The value you print can be a simple variable, or it can be an arbitrarily complicated Ruby
expression.

Comments

A tag with a hash mark right after the opening delimiter can hold comments, which arenʼt
interpreted as code and arenʼt displayed in the rendered output.

<%# This comment will be ignored. %>

Suppressing Line Breaks

Regular tags donʼt print anything, but if you keep each tag of logic on its own line, the line breaks
you use will show up as a swath of whitespace in the final file. If you donʼt like that, you can make
ERB trim the line break by putting a hyphen directly before the closing delimiter.

<% document += thisline -%>

Rendering a Template
To render output from a template, use Puppetʼs built-in template function:

file {'/etc/foo.conf':
ensure => file,
require => Package['foo'],
content => template('foo/foo.conf.erb'),
}

This evaluates the template and turns it into a string. Here, weʼre using that string as the content1
of a file resource, but like I said above, we could be using it for pretty much anything. Note that the
path to the template doesnʼt use the same semantics as the path in a puppet:/// URL2 — it should
be in the form <module name>/<path relative to module's templates directory>. (That is,
template('foo/foo.conf.erb') points to
/etc/puppetlabs/puppet/modules/foo/templates/foo.conf.erb.)

As a sidenote: if you give more than one argument to the template function…

Learning Puppet • Learning — Templates 35/51


template('foo/one.erb', 'foo/two.erb')

…it will evaluate each of the templates, then concatenate their outputs and return a single string.

An Aside: Other Functions

Since we just went over the template function, this is as good a time as any to cover functions in
general.

Most of the Puppet language consists of ways to say “Here is a thing, and this is what it is” —
resource declarations, class definitions, variable assignments, and the like. Functions are ways to
say “Do something.” Theyʼre a bucket for miscellaneous functionality. (You can even write new
functions in Ruby and distribute them in modules, if you need to repeatedly munge data or modify
your catalogs in some way.)

Puppetʼs functions are run during catalog compilation,3 and theyʼre pretty intuitive to call; itʼs
basically just function(argument, argument, argument), and you can optionally leave off the
parentheses. (Remember that include is also a function.) Some functions (like template) get
replaced with a return value, and others (like include) take effect silently.

You can read the full list of available functions at the function reference. We wonʼt be covering most
of these for a while, but you might find inline_template and regsubst useful in the short term.

An Example: NTP Again


So letʼs modify your NTP module to use templates instead of static config files.

First, weʼll change the init.pp manifest:

# init.pp

class ntp {
case $operatingsystem {
centos, redhat: {
$service_name = 'ntpd'
$conf_template = 'ntp.conf.el.erb'
$default_servers = [ "0.centos.pool.ntp.org",
"1.centos.pool.ntp.org",
"2.centos.pool.ntp.org", ]
}
debian, ubuntu: {
$service_name = 'ntp'
$conf_template = 'ntp.conf.debian.erb'
$default_servers = [ "0.debian.pool.ntp.org iburst",
"1.debian.pool.ntp.org iburst",
"2.debian.pool.ntp.org iburst",
"3.debian.pool.ntp.org iburst", ]
}

Learning Puppet • Learning — Templates 36/51


}

if $servers == undef {
$servers_real = $default_servers
}
else {
$servers_real = $servers
}

package { 'ntp':
ensure => installed,
}

service { 'ntp':
name => $service_name,
ensure => running,
enable => true,
subscribe => File['ntp.conf'],
}

file { 'ntp.conf':
path => '/etc/ntp.conf',
ensure => file,
require => Package['ntp'],
content => template("ntp/${conf_template}"),
}
}

There are several things going on, here:

We changed the File['ntp.conf'] resource, as advertised.

Weʼre storing the servers in an array, mostly so I can demonstrate how to iterate over an array
once we get to the template. If you wanted to, you could store them as a string with line breaks
and per-line server statements instead; it comes down to a combination of personal style and
the nature of the problem at hand.
Weʼll be using that $servers_real variable in the actual template, which might seem odd now
but will make more sense during the next chapter. Likewise with testing whether $servers is
undef. (For now, it will always be undef, as are all variables that havenʼt been assigned yet.)

Next, copy the config files to the templates directory, add the .erb extension to their names, and
replace the blocks of servers with some choice ERB code:

# ...

# Managed by Class['ntp']
<% servers_real.each do |server| -%>
server <%= server %>
<% end -%>

# ...

Learning Puppet • Learning — Templates 37/51


This snippet will iterate over each entry in the array and print it after a server statement, so, for
example, the string generated from the Debian template will end up with a block like this:

# Managed by Class['ntp']
server 0.debian.pool.ntp.org iburst
server 1.debian.pool.ntp.org iburst
server 2.debian.pool.ntp.org iburst
server 3.debian.pool.ntp.org iburst

You can see the limitations here — the servers are still basically hardcoded. But weʼve moved them
out of the config file and into the Puppet manifest, which gets us half of the way to a much more
flexible NTP class.

Next
And as for the rest of the way, keep reading to learn about parameterized classes.

1. This is a good time to remind you that filling a content attribute happens during catalog compilation, and serving a file with a
puppet:/// URL happens during catalog application. Again, this doesnʼt matter right now, but it may make some things
clearer later.↩

2. This inconsistency is one of those problems that tend to crop up over time when software grows organically. Weʼre working
on it, and you can keep an eye on ticket #4885 if that sort of thing interests you.↩

3. To jump ahead a bit, this means the agent never sees them.↩

Learning — Modules and (Parameterized)


Classes (Part Two)
Now that you have basic classes and modules under control, itʼs time for some more advanced code
re-use.

← Templates — Index — Defined Types →

Investigating vs. Passing Data


Most classes have to do slightly different things on different systems. You already know some ways
to do that — all of the modules youʼve written so far have switched their behaviors by looking up
system facts. Letʼs say that they “investigate:” they expect some information to be in a specific place
(in the case of facts, a top-scope variable), and go looking for it when they need it.

But this isnʼt always the best way to do it, and it starts to break down once you need to switch a
moduleʼs behavior on information that doesnʼt map cleanly to system facts. Is this a database
server? A local NTP server?

Learning Puppet • Learning — Modules and (Parameterized) Classes (Part Two) 38/51
You could still have your modules investigate; instead of looking at the standard set of system facts,
you could just point them to an arbitrary variable and make sure itʼs filled if you plan on using that
class. But it might be better to just tell the class what it needs to know when you declare it.

Parameters
When defining a class, you can give it a list of parameters.

class mysql ($user, $port) { ... }

This is a doorway for passing information into the class. When you declare the class, those
parameters appear as resource attributes; inside the definition, they appear as local variables.

# /etc/puppetlabs/puppet/modules/paramclassexample/manifests/init.pp
class paramclassexample ($value1, $value2 = "Default value") {
notify {"Value 1 is ${value1}.":}
notify {"Value 2 is ${value2}.":}
}

# ~/learning-manifests/paramclass1.pp
class {'paramclassexample':
value1 => 'Something',
value2 => 'Something else',
}

# ~/learning-manifests/paramclass2.pp
class {'paramclassexample':
value1 => 'Something',
}

# puppet apply ~/learning-manifests/paramclass1.pp


notice: Value 2 is Something else.
notice: /Stage[main]/Paramclassexample/Notify[Value 2 is Something
else.]/message: defined 'message' as 'Value 2 is Something else.'
notice: Value 1 is Something.
notice: /Stage[main]/Paramclassexample/Notify[Value 1 is Something.]/message:
defined 'message' as 'Value 1 is Something.'
notice: Finished catalog run in 0.05 seconds

# puppet apply ~/learning-manifests/paramclass2.pp


notice: Value 1 is Something.
notice: /Stage[main]/Paramclassexample/Notify[Value 1 is Something.]/message:
defined 'message' as 'Value 1 is Something.'
notice: Value 2 is Default value.
notice: /Stage[main]/Paramclassexample/Notify[Value 2 is Default
value.]/message: defined 'message' as 'Value 2 is Default value.'
notice: Finished catalog run in 0.05 seconds

(As shown above, you can give any parameter a default value, which makes it optional when you

Learning Puppet • Learning — Modules and (Parameterized) Classes (Part Two) 39/51
declare the class. Parameters without defaults are required.)

So whatʼs the benefit of all this? In a word, it encapsulates the class. You donʼt have to pick unique
magic variable names to use as a dead drop, and since anything affecting the function of the class
has to pass through the parameters, itʼs much more clear where the information is coming from.
This pays off once you start having to think about how modules work with other modules, and it
really pays off if you want to download or create reusable modules.

Example: NTP (Again)


So letʼs get back to our NTP module. The first thing we talked about wanting to vary was the set of
servers, and we already did the heavy lifting back in the templates chapter, so thatʼs a good place to
start:

class ntp ($servers = undef) {


...

And… thatʼs all it takes, actually. This will work. If you declare the class with no attributes…

class {'ntp':}

…itʼll work the same way it used to. If you declare it with a servers attribute containing an array of
servers (with or without appended iburst and dynamic statements)…

class {'ntp':
servers => [ "ntp1.puppetlabs.lan dynamic", "ntp2.puppetlabs.lan
dynamic", ],
}

…itʼll override the servers in the ntp.conf file. Nice.

There is a bit of trickery to notice: setting a variable or parameter to undef might seem odd, and
weʼre only doing it because we want to be able to get the default servers without asking for them.
(Remember, parameters canʼt be optional without an explicit default value.)

Also, remember the business with the $servers_real variable? That was because the Puppet
language wonʼt let us re-assign the $servers variable within a given scope. If the default value we
wanted was the same regardless of OS, we could just use it as the parameter default, but the extra
logic to accomodate the per-OS defaults means we have to make a copy.

While weʼre in the NTP module, what else could we make into a parameter? Well, letʼs say you have a
mixed environment of physical and virtual machines, and some of them occasionally make the
transition between VM and metal. Since NTP behaves weirdly under virtualization, youʼd want it

Learning Puppet • Learning — Modules and (Parameterized) Classes (Part Two) 40/51
turned off on your virtual machines — and you would have to manage the service as a resource to
do that, because if you just didnʼt say anything about NTP (by not declaring the class, e.g.), it might
actually still be running. So you could make a separate ntp_disabled class and declare it whenever
you arenʼt declaring the ntp class… but it makes more sense to expose the serviceʼs attributes as
class parameters. That way, when you move a formerly physical server into the cloud, you could just
change that part of its manifests from this:

class {'ntp':}

…to this:

class {'ntp':
ensure => stopped,
enable => false,
}

And making that work right is almost as easy as the last edit. Hereʼs the complete class, with all of
our modifications thus far:

#/etc/puppetlabs/puppet/modules/ntp/manifests/init.pp
class ntp ($servers = undef, $enable = true, $ensure = running) {
case $operatingsystem {
centos, redhat: {
$service_name = 'ntpd'
$conf_template = 'ntp.conf.el.erb'
$default_servers = [ "0.centos.pool.ntp.org",
"1.centos.pool.ntp.org",
"2.centos.pool.ntp.org", ]
}
debian, ubuntu: {
$service_name = 'ntp'
$conf_template = 'ntp.conf.debian.erb'
$default_servers = [ "0.debian.pool.ntp.org iburst",
"1.debian.pool.ntp.org iburst",
"2.debian.pool.ntp.org iburst",
"3.debian.pool.ntp.org iburst", ]
}
}

if $servers == undef {
$servers_real = $default_servers
}
else {
$servers_real = $servers
}

package { 'ntp':
ensure => installed,
}

Learning Puppet • Learning — Modules and (Parameterized) Classes (Part Two) 41/51

service { 'ntp':
name => $service_name,
ensure => $ensure,
enable => $enable,
subscribe => File['ntp.conf'],
}

file { 'ntp.conf':
path => '/etc/ntp.conf',
ensure => file,
require => Package['ntp'],
content => template("ntp/${conf_template}"),
}
}

Is there anything else we could do to this class? Well, yes: its behavior under anything but Debian,
Ubuntu, CentOS, or RHEL is currently undefined, so itʼd be nice to, say, come up with some config
templates to use under the BSDs and OS X and then fail gracefully on unrecognized OSes. And it
might make sense to unify our two current templates; they were just based on the system defaults,
and once you decide how NTP should be configured at your site, chances are itʼs going to look
similar on any Unix. This could also let you simplify the default value and get rid of that undef and
$servers_real dance. But as it stands, this module is pretty serviceable.

So hey, letʼs throw on some documentation and be done with it!

Module Documentation
# = Class: ntp
#
# This class installs/configures/manages NTP. It can optionally disable NTP
# on virtual machines. Only supported on Debian-derived and Red Hat-derived
OSes.
#
# == Parameters:
#
# $servers:: An array of NTP servers, with or without +iburst+ and
# +dynamic+ statements appended. Defaults to the OS's defaults.
# $enable:: Whether to start the NTP service on boot. Defaults to true.
Valid
# values: true and false.
# $ensure:: Whether to run the NTP service. Defaults to running. Valid
values:
# running and stopped.
#
# == Requires:
#
# Nothing.
#
# == Sample Usage:
#
# class {'ntp':

Learning Puppet • Learning — Modules and (Parameterized) Classes (Part Two) 42/51
# servers => [ "ntp1.puppetlabs.lan dynamic",
# "ntp2.puppetlabs.lan dynamic", ],
# }
# class {'ntp':
# enable => false,
# ensure => stopped,
# }
#
class ntp ($servers = undef, $enable = true, $ensure = running) {
case $operatingsystem { ...
...

This doesnʼt have to be Tolstoy, but seriously, at least write down what the parameters are and
what kind of data they take. Your future self will thank you. Also! If you write your documentation in
RDoc format and put it in a comment block butted up directly against the start of the class
definition, you can automatically generate a browsable Rdoc-style site with info for all your
modules. You can test it now, actually:

# puppet doc --mode rdoc --outputdir ~/moduledocs --modulepath


/etc/puppetlabs/puppet/modules

(Then just upload that ~/moduledocs folder to some webspace you control, or grab it onto your
desktop with SFTP.)

Some Important Notes From the Depʼt of Foreshadowing


Parameterized classes are still pretty new — they were only added to Puppet in version 2.6.0 — and
they changed the landscape of Puppet in some ways that arenʼt immediately obvious.

You probably noticed that the examples in this chapter are all using the resource-like declaration
syntax instead of the include function. Thatʼs because include doesnʼt work1 with parameterized
classes, and likely never will. The problem is that the whole point of include conflicts with the idea
that a class can change depending on how itʼs declared — if you declare a class multiple times and
the attributes donʼt match precisely, which set of attributes wins?

Parameterized classes made the problem with that paradigm more explicit, but it already existed,
and it was possible to run afoul of it without even noticing. A common pattern for passing
information into a class was to choose an external variable and have the class retrieve it with
dynamically-scoped variable lookup.2 If you were also having low-level classes manage their own
dependencies by including anything they might need, then a given class might have several
potential scope chains resolving to different values, which would result in a race — whichever
include took effect first would determine the behavior of the class.

However, there were and are a couple of other ways to get data into a class — letʼs lump them
together and call them data separation — and if you used them well, your classes could safely

Learning Puppet • Learning — Modules and (Parameterized) Classes (Part Two) 43/51
manage their own dependencies with include. Using parameterized classes gives you new options
for site design that weʼve come to believe are just plain better, but it closes off that option of self-
managed dependencies.

Iʼm purposely getting ahead of myself a bit — this isnʼt going to be fully relevant until we start
talking about class composition and site design, and weʼll be covering data separation later as well.
But since all these issues stem from ideas about what a class is and where it gets its information, it
seemed worthwhile to mention some of these issues now, just so they donʼt seem so out-of-the-
blue later.

Next
Okay, we can pass parameters into classes now and change their behavior. Great! But classes are
still always singletons; you canʼt declare more than one copy and get two different sets of behavior
simultaneously. And youʼll eventually want to do that! What if you had a collection of resources that
created a vhost definition for a web server, or cloned a Git repository, or managed a user account
complete with group, SSH key, home directory contents, sudoers entry, and .bashrc/.vimrc/etc.
files? What if you wanted more than one Git repo, user account, or vhost on a single machine?

Well, youʼd whip up a defined resource type.

1. Yes, you can actually include a parameterized class if all of its parameters have defaults, but mixing and matching
declaration styles for a class is not the best plan.↩

2. I havenʼt covered dynamic scope in Learning Puppet, both because it shouldnʼt be necessary for someone learning today and
because its days are numbered.↩

Learning — Defined Types (Modules, Part


Three)
Use defined resource types to group basic resources into super-resources.

← Parameterized Classes — Index — TBA →

Beyond Singletons
Classes are good for modeling singleton aspects of a system, but to model repeatable chunks of
configuration — like a Git repository or an Apache vhost — you should use defined resource types.

Defined types just act like normal resource types, and are declared in the same way…

apache::vhost {'personal_site':
port => 80,
docroot => '/var/www/personal',
options => 'Indexes MultiViews',
Learning Puppet • Learning — Defined Types (Modules, Part Three) 44/51
}

…but under the hood, theyʼre composed of other resources.

Defining a Type
You define a type with the define keyword, and the definition looks almost exactly like a
parameterized class. You need:

A name
A list of parameters (in parentheses, after the name)
(Defined types also get a special $title parameter without having to declare it, and its value
is always set to the title of the resource instance. Classes get this too, but itʼs less useful since
a class will only ever have one name.)

And a collection of resources.

Like this:

define planfile ($user = $title, $content) {


file {"/home/${user}/.plan":
ensure => file,
content => $content,
mode => 0644,
owner => $user,
require => User[$user],
}
}

user {'nick':
ensure => present,
managehome => true,
uid => 517,
}
planfile {'nick':
content => "Working on new Learning Puppet chapters. Tomorrow: upgrading
the LP virtual machine.",
}

This oneʼs pretty simple. (In fact, itʼs basically just a macro.) It has two parameters, one of which is
optional (it defaults to the title of the resource), and the collection of resources it declares is just a
single file resource.

“
A quick note: If your VM is running Puppet 2.6.4 (use puppet --version to find
out), that example won’t work as written, because $title was exposed to the
parameter list in Puppet 2.6.5. You’ll need to either upgrade the VM to Puppet

Learning Puppet • Learning — Defined Types (Modules, Part Three) 45/51


“Enterprise 1.2, or make the $user parameter mandatory by removing the
default. You can still use the $title parameter as a variable inside the
definition, though.

Special Little Flowers


So itʼs pretty simple, right? Exactly like defining a class? Almost: thereʼs one extra requirement.
Since the user might declare any number of instances of a defined type, you have to make sure that
the implementation will never declare the same resource twice.

Consider a slightly different version of that first definition:

define planfile ($user = $title, $content) {


file {'.plan':
path => "/home/${user}/.plan",
ensure => file,
content => $content,
mode => 0644,
owner => $user,
require => User[$user],
}
}

See how the title of the file resource isnʼt tied to any of the definitionʼs parameters?

planfile {'nick':
content => "Working on new Learning Puppet chapters. Tomorrow: upgrading
the LP virtual machine.",
}

planfile {'chris':
content => "Resurrecting a very dead laptop.",
}

# puppet apply planfiles.pp


Duplicate definition: File[.plan] is already defined in file
/root/manifests/planfile.pp at line 9; cannot redefine at
/root/manifests/planfile.pp:9 on node puppet.localdomain

Yikes. You can see where we went wrong — every time we declare an instance of planfile, itʼs
going to declare the resource File['.plan'], and Puppet will fail compilation if you try to declare
the same resource twice.

Learning Puppet • Learning — Defined Types (Modules, Part Three) 46/51


To avoid this, you have to make sure that both the title and the name (or namevar) of every
resource in the definition are somehow derived from a unique parameter (often the $title) of the
defined type. (For example, we couldnʼt derive the fileʼs title from the $content of the planfile
resource, because more than one user might write the same .plan text.)

If thereʼs a singleton resource that has to exist for any instance of the defined type to work, you
should:

Put that resource in a class, and…


…either use include to declare that class in the type definition, or use the following idiom in the
definition to make sure itʼll fail without the class:

Class['myclass'] -> Defined::Type["$title"]

Defined Types in Modules


Defined types can be autoloaded just like classes, and thus used from anywhere in your manifests.
Like with classes, each defined type should go in its own file in a moduleʼs manifests/ directory,
and the same rules for namespacing apply. (So the apache::vhost type would go somewhere like
/etc/puppetlabs/puppet/modules/apache/manifests/vhost.pp, and if we were to keep the

Learning Puppet • Learning — Defined Types (Modules, Part Three) 47/51


planfile type around, it would go in
/etc/puppetlabs/puppet/modules/planfile/manifests/init.pp.)

Resource References and Namespaced Types

You might have already noticed this above, but: when you make a resource reference to an instance
of a defined type, you have to capitalize every namespace segment in the typeʼs name. That means
an instance of the foo::bar::baz type would be referenced like Foo::Bar::Baz['mybaz'].

An Example: Apache Vhosts


Not that my .plan macro wasnʼt pretty great, but letʼs be serious for a minute. Remember your
Apache module from a few chapters back? Letʼs extend it so we can easily declare vhosts. (Big
thanks to the ops team here at Puppet Labs, from whom I borrowed this code.)

# Definition: apache::vhost
#
# This class installs Apache Virtual Hosts
#
# Parameters:
# - The $port to configure the host on
# - The $docroot provides the DocumentationRoot variable
# - The $template option specifies whether to use the default template or
override
# - The $priority of the site
# - The $serveraliases of the site
# - The $options for the given vhost
# - The $vhost_name for name based virtualhosting, defaulting to *
#
# Actions:
# - Install Apache Virtual Hosts
#
# Requires:
# - The apache class
#
# Sample Usage:
# apache::vhost { 'site.name.fqdn':
# priority => '20',
# port => '80',
# docroot => '/path/to/docroot',
# }
#
define apache::vhost(
$port,
$docroot,
$template = 'apache/vhost-default.conf.erb',
$priority = '25',
$servername = '',
$serveraliases = '',
$options = "Indexes FollowSymLinks MultiViews",
$vhost_name = '*'
) {

Learning Puppet • Learning — Defined Types (Modules, Part Three) 48/51



include apache

# Below is a pre-2.6.5 idiom for having a parameter default to the title,
# but you could also just declare $servername = "$title" in the
parameters
# list and change srvname to servername in the template.

if $servername == '' {
$srvname = $title
} else {
$srvname = $servername
}
case $operatingsystem {
'centos', 'redhat', 'fedora': { $vdir = '/etc/httpd/conf.d'
$logdir = '/var/log/httpd'}
'ubuntu', 'debian': { $vdir = '/etc/apache2/sites-enabled'
$logdir = '/var/log/apache2'}
default: { $vdir = '/etc/apache2/sites-enabled'
$logdir = '/var/log/apache2'}
}
file {
"${vdir}/${priority}-${name}.conf":
content => template($template),
owner => 'root',
group => 'root',
mode => '755',
require => Package['httpd'],
notify => Service['httpd'],
}

}

# /etc/puppetlabs/modules/apache/templates/vhost-default.conf.erb

# ************************************
# Default template in module puppetlabs-apache
# Managed by Puppet
# ************************************

Listen <%= port %>
NameVirtualHost <%= vhost_name %>:<%= port %>
<VirtualHost <%= vhost_name %>:<%= port %>>
ServerName <%= srvname %>
<% if serveraliases.is_a? Array -%>
<% serveraliases.each do |name| -%><%= " ServerAlias #{name}\n" %><% end -
%>
<% elsif serveraliases != '' -%>
<%= " ServerAlias #{serveraliases}" -%>
<% end -%>
DocumentRoot <%= docroot %>
<Directory <%= docroot %>>
Options <%= options %>
AllowOverride None

Learning Puppet • Learning — Defined Types (Modules, Part Three) 49/51


Order allow,deny
allow from all
</Directory>
ErrorLog <%= logdir %>/<%= name %>_error.log
LogLevel warn
CustomLog <%= logdir %>/<%= name %>_access.log combined
ServerSignature Off
</VirtualHost>

And thatʼs more or less a wrap. You can apply a manifest like this:

apache::vhost {'testhost':
port => 8081,
docroot => '/var/www-testhost',
priority => 25,
servername => 'puppet',
}

…and (as long as the directory exists) youʼll immediately be able to reach the new vhost:

# curl http://puppet:8081

In a way, this is just slightly more sophisticated than the first example — itʼs still only one file
resource — but the use of a template makes it a LOT more powerful, and you can already see how
much time it can save. And you can make it slicker as you build more types: once youʼve got a
custom type that handles firewall rules, for example, you can add something like this to the
definition:

firewall {"0100-INPUT ACCEPT $port":


jump => 'ACCEPT',
dport => "$port",
proto => 'tcp'
}

Exercises
Take a minute to make a few more defined types, just to get used to modeling repeatable groups of
resources.

Try wrapping a user resource in a human::user type that automatically grabs that personʼs
.bashrc file from your site module and manages one or more ssh_authorized_key resources
for their account.
If youʼre familiar with git, take a stab at writing a git::repo type that can clone from a repository
on the network (and maybe even keep the working copy up-to-date on a specific branch!). Thisʼll
be harder — youʼll probably have to make a git class to make sure git is available, and youʼll

Learning Puppet • Learning — Defined Types (Modules, Part Three) 50/51


have to use at least one file (ensure => directory) and at least one exec resource.

One Last Tip


Defined types take input, and input can get a little dirty — you might want to check your parameters
to make sure theyʼre the correct data type, and fail early if theyʼre garbage instead of writing
undefined stuff to the system.

If youʼre going to make a practice of validating your inputs (hint: DO), you can save yourself a lot of
effort by using the validation functions in Puppet Labsʼ stdlib module. We ship a version of stdlib
with PE 1.2, and you can also download it for free at either GitHub and the module forge. The
functions are:

validate_array

validate_bool

validate_hash

validate_re

validate_string

You can learn how to use these by running puppet doc --reference function | less on a
system that has stdlib installed in its modulepath, or you can read the documentation directly in
each of the functionsʼ files — look in the lib/puppet/parser/functions directory of the module.

Next
Okay. Thereʼs more to say about modules — we still havenʼt covered data separation, patterns for
making your modules more readable, or module composition yet — but thereʼs more important
business afoot. Come back next time for master/agent Puppet!
© 2010 Puppet Labs [email protected] 411 NW Park Street / Portland, OR 97209 1-877-575-
9775

Learning Puppet • Learning — Defined Types (Modules, Part Three) 51/51

You might also like