The Puppet Labs Issue Tracker has Moved: https://tickets.puppetlabs.com

Stanford Puppet Best Practices

Authors: Digant C Kasundra digant@stanford.edu

Please note: This represents a point in time, and the approach of one set of Puppet users. With the release of Puppetforge, a work is in progress to produce a standards and style guide that is up-to-date, and represents the consensus of the Puppet commuity.

Introduction

To effectively maintain a large number of systems, Puppet is essential to keep the systems in a consistent state. Often, Puppet manifests will be written by multiple system administrators to manage several dozen types of systems. These standards and best practices are presented here as an evolving effort to document and architect the Puppet service in a manageable fashion in such a large environment. One should also review the Style Guide .

These best practices were developed at Stanford University with contributions from the greater Puppet community and represent the embodiment of two years of Puppet infrastructure deployment and management.

Terminology

The terminology used in the Puppet community can be found at the official Glossary Of Terms .

Change Log

  • A dist area is no longer used, as was recommended in the previous Best Practice. All distributable files should be managed in the files subdir of the logically associated module.

File Hierarchy

All Puppet data files (modules, manifests, distributable files, etc) should be maintained in a Subversion or CVS repository (or your favorite Version Control System). The following hierarchy describes the layout one should use to arrange the files in a maintainable fashion:

  • /manifests/: this directory contains files and subdirectories that determine the manifest of individual systems but do not logically belong to any particular module. Generally, this directory is fairly thin and alternatives such as the use of LDAP or other external node tools can make the directory even thinner. This directory contains the following special files:

    • site.pp: first file that the Puppet Master parses when determining a server’s catalog. It imports all the underlying subdirectories and the other special files in this directory. It also defines any global defaults, such as package managers. See Puppet Best Practice2 .
    • templates.pp: defines all template classes. See also Glossary Of Terms . See [[Puppet_Best_Practice2#sample-templates-pp|Puppet Best Practice2]] .
    • nodes.pp: defines all the nodes if not using an external node tool. See [[Puppet_Best_Practice2#sample-nodes-pp|Puppet Best Practice2]] .
  • /modules/{modulename}/: houses puppet modules in subdirectories with names matching that of the module name. This area defines the general building blocks of a server and contains modules such as for openssh, which will generally define classes openssh::client and openssh::server to setup the client and server respectively. The individual module directories contains subdirectories for manifests, distributable files, and templates. Module Organisation , [[Glossary_Of_Terms#module|Glossary Of Terms]] .

  • /modules/user/: A special module that contains manifests for users. This module contains a special subclass called user::virtual which declares all the users that might be on a given system in a virtual way. The other subclasses in the user module are classes for logical groupings, such as user::unixadmins, which will realize the individual users to be included in that group. See also [[Puppet_Best_Practice2#naming-conventions|Puppet Best Practice2]] , [[Glossary_Of_Terms#realize|Glossary Of Terms]] .
  • /services/ : this is an additional modules area that is specified in the module path for the puppetmaster. However, instead of generic modules for individual services and bits of a server, this module area is used to model servers specific to enterprise level infrastructure services (core infrastructure services that your IT department provides, such as www, enterprise directory, file server, etc). Generally, these classes will include the modules out of /modules/ needed as part of the catalog (such as openssh::server, postfix, user::unixadmins, etc). The files section for these modules is used to distribute configuration files specific to the enterprise infrastructure service such as openldap schema files if the module were for the enterprise directory. To avoid namespace collision with the general modules, it is recommended that these modules/classes are prefixed with s_ (e.g. s_ldap for the enterprise directory server module)
  • /clients/: similar to the /services/ module area, this area is used for modules related to modeling servers for external clients (departments outside your IT department). To avoid namespace collision, it is recommended that these modules/classes are prefixed with c_.
  • /notes/: this directory contains notes for reference by local administrators.
  • /plugins/: contains custom types programmed in Ruby. See also Glossary Of Terms .
  • /tools/: contains scripts useful to the maintenance of Puppet.

Naming Conventions

For consistency, legibility, and avoidance of classname conflicts and collisions, a naming convention has been adopted as specified below.

  • nodes: all nodes must have a name equal to their hostname.
  • overrides that disable: when overriding a class that provides a service for the explicit purpose of providing a subclass that disables that service, that subclass should be named disabled, such as ssh::disabled. See also Puppet Best Practice2 .

Other naming conventions may be drafted as needed in future.

Managing Users

NOTE: in situations where there isn’t such a need to dramatically separate users or use virtual users, using ralsh to convert a passwd file into user classes is much easier.

Declaring affiliated users

Users (regular employees, students, etc) should be declared virtually in a a class called user::virtual. This can be done in an automated fashion if a source-of-record or master passwd file exists, using the [[Puppet_Best_Practice2#conv-passwd|Puppet Best Practice2]] tool.

This approach helps minimize the number of files maintained in the users directory, which is important in a large institution. These virtual users can then be Realized as needed in user groups (discussed below). See also Puppet Best Practice2 .

Declaring Application Users

Application users (such as the oracle user) should have their own individual class in which their user resource is declared and reside in the users module (see also Puppet Best Practice2 ).

This approach makes it easy to find and clean these temporary users during audits.

Declaring User Groups

Most systems will have users that can be logically grouped together. As an example, all systems might have the Unix systems administrators as part of their catalog. Therefore, a user group should exist that contains these users as members and be included as part of the basenode template.

To create such a user grouping, create a class in the user module. For instance, create the class user::unixadmins as file unixadmins.pp in the directory /modules/users/manifests. (see also Puppet Best Practice2 ). The class should use the Realize directive to indicate which virtual users should be associated with this user group. See [[Puppet_Best_Practice2#sample-unixadmins-pp|Puppet Best Practice2]] .

Overriding Elements

There are times when the default parameters of a resource are not sufficient or accurate in a particular node or class. At such times, those parameter’s must be overridden but those overrides must be declared in a way such that they do not accidentally end up in the catalogs of other nodes. Moreover, care must be taken such that the overriding class has a unique name. As such, a new subclass is created.

This new class should inherit the class that declared the original resource and should be named with a prefix equal to the class or node that will use it (or an abbreviated equivalent).

Example:

class somehost_postfix inherits postfix {
    # blah blah blah
}

node somehost {
    include somehost_postfix
}

If the subclass is actually a specialized version of the superclass to be consumed by no one particular node or class but rather be available to many, then this subclass should be part of the module. For instance, the ssh module contains the openssh class, the openssh::global subclass, the openssh::disabled subclass, etc (declared in the files init.pp, global.pp, disabled.pp respectively to allow magic name-to-manifest mapping: Module Organisation ).

Syntax and Formatting

For stylistic recommendations on Syntax and Formatting, please take a look at the Style Guide .

Classes vs. Defined Types

Classes and Defined Types (or definitions) seem almost interchangable, but there is a distinct difference: classes are singletons and definitions can be declared multiple times (with differing parameters), just as types can be declared multiple times (as long as they manage unique resources).

For instance, a generic webserver would be a class. You would include that class as part of any node that needed to be built as a generic webserver. That class would drop in whatever packages, etc, it needed to do.

But to manage iptables, you would rarely have a single class that handled all of the rules because your firewall rules would vary based on function of the server. Moreover, using overrides in this case would be hectic because the number of different combinations of iptables rules might be rather large. Instead, you could use a script to build iptables based on fragments found in a directory. And you could create a defined-type that would be responsible for dropping in a particular fragment per declaration. You would declare as many of the defined-type instances as necessary: one per fragment. This is what makes defined-types powerful.

Appendix

sample nodes.pp

# nodes.pp
#

node 'someserver.domain.com' inherits basenode {
    $web_fqdn = 'www.domain.com'
    include genericwebserver
    include some_other_service
}

node 'ldapmaster.domain.com' inherits basenode {
    include s_ldap::master
}

node 'humanresources.domain.com' inherits basenode {
    include c_humanresources
}

sample site.pp

# site.pp

import "templates"
import "nodes"

filebucket { main: server => puppet }

# global defaults
File { backup => main }
Exec { path => "/usr/bin:/usr/sbin/:/bin:/sbin" }

Package {
    provider => $operatingsystem ? {
        debian => aptitude,
        redhat => up2date
    }
}

sample unixadmins.pp in the user module

System Message: WARNING/2 (<string>, line 219)

Title underline too short.

sample unixadmins.pp in the user module
--------------------------------------

# unixadmins.pp
#
# Realize the members of the Unix team and include any contractors

class user::unixadmins inherits user::virtual {
    # Realize our team members
    realize(
        User["aguy"],
        User["agirl"]
    )
}

sample templates.pp

# templates.pp
# All server templates for various flavors of templates defined here

class baseclass {
    include $operatingsystem,
        afs,
        cron,
        dns,
        puppetclient,
        ssh,
        unixadmin_users,
        user_root,
        virt_all_users

    package { "ourcompany-package": ensure => present }
}

node default {
    include baseclass
}

class genericwebserver
    include baseclass
    include apache, apachelocal
}

class oracleserver {
    include baseclass
    include oracledb, patrol
}

sample virtual.pp in users module

# virtual.pp
#
# People accounts of interest as virtual resources

class user::virtual {
    @user { "aguy":
        ensure  => "present",
        uid     => "1001",
        gid     => "1001",
        comment => "Andrew Guy",
        home    => "/afs/ir/users/a/g/aguy",
        shell   => "/bin/bash",
    }

    @user { "agirl":
        ensure  => "present",
        uid     => "1002",
        gid     => "137",
        comment => "Anita Girl",
        home    => "/afs/ir/users/a/g/agirl",
        shell   => "/bin/zsh",
    }
  [...]
}

conv_passwd

This script is used to convert a specified passwd file into the user::virtual class referenced in the Puppet Best Practice2 section above.

#!/usr/bin/ruby
#
# This simple program reads in a passwd file and converts it to a
# virt_all_users file

if ARGV[0] == nil
    puts "Must specify a passwd file to use."
    puts "Generally, you'll want to pipe the output to the virt_all_users.pp manifest"
    exit
end

pwdFile = ARGV[0]

puts <<HEADER
# Fully sponsored accounts of interest as virtual resources

class user::virtual {
HEADER

File.open(pwdFile) do |file|
    while line = file.gets
        sunetid, tmp, uid, gid, name, homedir, shell = line.split(/\:/)
        puts "    @user { \"" + sunetid + "\":"
        puts "        ensure  => \"present\","
        puts "        uid     => \"" + uid + "\","
        puts "        gid     => \"" + gid + "\","
        puts "        comment => \"" + name + "\","
        puts "        home    => \"" + homedir + "\","
        puts "        shell   => \"" + shell.chomp + "\","
        puts "    }"
        puts ""
    end
end

puts <<FOOTER
}
FOOTER

%reaper %orangecone — We should delete this page now, and email Digant and team to ask if they’d like us to redirect the URL to anywhere in particular.