Simple Puppet Module Structure

09/28/2009

Note: While this post is still relevant, you should also check the newer Simple Puppet Module Structure Redux post

Puppet supports something called Modules, it’s a convenient collection of templates, files, classes, types and facts all related to one thing.  I find it makes it easy to think about a problem in an isolated manner and it should be the default structure everyone use.

Usually though the problem comes with how to lay out modules and how to really use the Puppet classes system, below a simple module layout that can grow with you as your needs extend.  I should emphasis the simple here – this example does not cater well for environments with mix of Operating Systems, or weird multi site setups with overrides per environment, but once you understand the design behind this you should be able to grow it to cater for your needs.  If you have more complex needs, see this post as a primer on using the class relationships effectively.

For the most part I find I make modules for each building block – apache, syslog, php, mysql – usually the kind of thing that has:

  • One or more  packages – or at least some install process.
  • One or more config files or config actions
  • A service or something that you do to start it.

You can see there’s an implied dependency tree here, the config steps require the package to be installed and the service step require the config to be done.  Similarly if any config changes the services might need to restart.

We’d want to cater for the case where you could have 10 config files, or 10 packages, or 100s of files needed to install the service in question.  You really wouldn’t want to create a big array of require => resources to control the relationships or to do the notifications, this would be a maintenance nightmare.

We’re going to create a class for each of the major parts of the module – package, config and service.  And we’ll use the class systems and relationships, notifies etc to ensure the ordering, requires and notifies are kept simple – especially so that future changes can be done in one place only without editing everywhere else that has requires.

class ntp::install {
    package{"ntpd":
        ensure => latest
    }
}
 
class ntp::config {
    File{
        require => Class["ntp::install"],
        notify  => Class["ntp::service"],
        owner   => "root",
        group   => "root",
        mode    => 644
    }
 
    file{"/etc/ntp.conf":
            source => "puppet:///ntp/ntp.conf";
 
         "/etc/ntp/step-tickers":
            source => "puppet:///ntp/step-tickers";
    }
}
 
class ntp::service {
    service{"ntp":
        ensure  => running,
        enable  => true,
        require => Class["ntp::config"],
    }
}
 
class ntp {
    include ntp::install, ntp::config, ntp::service
}

If we now just do include ntp everything will happen cleanly and in order.

There’s a few things you should notice here.  We’re using the following syntax in a few places:

  • require => Class["ntp::something"]
  • notify => Class["ntp::service"]
  • before => Class["ntp::something"]
  • subscribe => Class["ntp::config"]

All of the usual relationship meta parameters can operate on classes, so what this means is if you require Class["ntp::config"] then both ntp.conf and step-tickers will be created before the service starts without having to list the files in your relationships.  Similarly if you notify a class then all resources in that class gets notifies.

The preceding paragraph is very important to grasp, typically we’d try to maintain arrays of files in require trees and later if we add a new config file we have to go hunt everywhere that needs to require it and update those places, by using this organization approach we never have to update anything.   If I add 10 services and 100 files into the config and service classes everything else that requires ntp::service will still work as expected without needing updates.

This is a very important decoupling of class contents with class function.  Your other classes should never be concerned with class contents only class function.

As a convention this layout is a good sample, if all your daemons gets installed in the daemon::install class you’ll have no problem knowing where to find the code to adjust the install behavior – perhaps to upgrade to the next version – you should try to come up with a set of module naming conventions that works for you and stick to it throughout your manifests – I have a rake task to setup my usual module structure, they’re all the same and the learning curve for new people is small.

By building up small classes that focus on their task or subtask you can build up ever more powerful modules.  We have a simple example here, but you can see how you could create ntp::master and ntp::client wrapper classes that would simply reuse the normal ntp::install and ntp::service classes.

As I mentioned this is a simple example for simple needs, the real take away here is to use class relationships and decouple the contents of those classes with their intent this should give you a more maintainable code set.

Ultimately conventions such as these will be needed to get us all one step closer to being able to share modules amongst each other as the specifics of making Apache on different distributions work will not matter – we’d just know that we want Class["apache::service"] for example.