Make Puppet Agnostic Again!

Having spent a handful of years managing multiple implementations of Puppet, I thought it’d be nice to dump some of my learnings into an article.

After a couple years of writing and rewriting various modules, the main points I’ve taken away from it all is to make them agnostic and modular.

By agnostic I mean the module should give you the ability to support multiple operating systems or at the very least allow you to incorporate them easily. In terms of modular, I mean split up your module into multiple manifests rather than a singular monolithic one.


Agnostic#

One method I take to ensure my module agnostic is via the use of conditional statements and expressions inside of a params.pp file.

All OS specific information is stored inside of variables which are then separated via conditionals within the params.pp, and then each manifest within the module references the variables. By doing so you are only referencing the one variable while the value of the variable will then differ depending on which conditional it meets. If I’m not making much sense, here’s an example:

class puppet::params {

  # Universal config
  $service_name = "puppet"
  $package_name = "puppet-agent"

  case $::osfamily {
    'Ubuntu': {
      $package_version       = "1.5.0-1"
    }
    'RedHat': {
      $package_version       = "1.5.0-2"
    }
    default: {
      fail("Module ${module_name} is not supported on ${::operatingsystem}")
    }
  }
}

While there is some basic config that will be shared outside of the case statement, (in this instance) the package versions vary. Therefore via the use of case statements you can then set the package versions depending on which OS the catalog is being applied to.

By using this approach you are able to add support for more operating systems in a simpler and cleaner fashion.

Finally the default condition then allows you to fail the installation of the module if you’re trying to install it on an unsupported OS, while giving you an easy to read error message for debugging.

Another great thing about this method is that all your variables go into one manifest, this then means you only have one place to manage it. All of your other manifests within the module refer to to your params.pp so the rest of your manfiests are able to stay uniform, and don’t contain anything other than the resource statements.


Modular#

Instead of having a huge monolithic manifest that becomes a ballache to maintain, it’s a lot nicer to break it up. By doing so you can make it easier to decouple the components of the module and make it easier to reference each individual manifest without having to apply everything in one go. Here’s an example of the remaining manifests for the Puppet module:

init.pp

class puppet (
  $puppetmaster  	  = $puppet::params::puppetmaster,
  $package_version 	  = $puppet::params::puppet_version,
  $package_name   	  = $puppet::params::package_name,
  $service_name  	  = $puppet::params::service_name
) inherits puppet::params {

  include ::puppet::client
  include ::puppet::service
}

client.pp

class puppet::install (
  $package_version = $puppet::puppet_version,
  $package_name    = $puppet::package_name,
  $service_name    = $puppet::service_name
) {

  # Install puppet and place puppet.conf onto the machine
  package {$package_name:
    ensure => $package_version,
    notify => Service[$service_name]
  }
}

service.pp

class puppet::service (
  $package_name    = $puppet::package_name,
  $service_name    = $puppet::service_name
) {

  service {$service_name:
    ensure  => running,
    enable  => true,
    require => Package[$package_name]
  }
}

A counter argument to this is, when does it become too modular? Approaching a module with this mindest, your number of manifests could get out of hand and start becoming a ballache to manage again. Definitely worth keeping that in mind when you see yourself going down a bit of a rabbit hole.


Puppet Forge#

Before writing any module it’s definitely worth taking a look at Puppet Forge to see if there’s a module out there at will do what you’re looking to achieve. If you use one of the popular modules they are already tried and tested, and if they need any tweaking most will accept sane pull requests. Yay open source.


Hiera#

While I haven’t included this in my examples above, it’s certainly good practise to utilise Hiera. Here’s what Puppet have to say about it:

Hiera makes Puppet better by keeping site-specific data out of your manifests. Puppet classes can request whatever data they need, and your Hiera data will act like a site-wide config file.

This makes it:

  • Easier to configure your own nodes: default data with multiple levels of overrides is finally easy.
  • Easier to re-use public Puppet modules: don’t edit the code, just put the necessary data in Hiera.
  • Easier to publish your own modules for collaboration: no need to worry about cleaning out your data before showing it around, and no more clashing variable names.

Another huge positive to Hiera is the fact that it encrypts all the values set in your YAML files. If that’s not a reason to get using Hiera then what is.


Final Thoughts#

Everything I’ve learnt is from practical experience and imitating popular community modules so please do not take any of this as gospel, or even best practise.

Another thing to keep in mind is that this implementation is a more traditional approach, and isn’t utilising the roles and profiles pattern. I would most certainly recommend using Hiera wherever possible, and follow the profiles and roles approach.

That being said I hope the examples above helps anyone looking to make their Puppet a bit more modular and agnostic.