First of all, it is worth mentioning that this is not an introductory post to Puppet. For that reason, it is advisable to have prior knowledge of the language before you read the following lines.
That said, let us begin.
What is a module?
Puppet handles ‘something’ called ‘modules’, fair enough, but; what are modules? One way to think about them is as a set of Types, Resources, Classes, Templates and Facts; all of these are related to each other in order to approach a specific issue in an individual fashion.
What we always have to take into account
When it is time to write a module, we must always have a clear idea of its intended scope. It is very common to go off track by adding features that exceed the requirements to address the issue at hand. In other words: ‘KISS‘. 😉
Blueprints
As we develop new modules, we find scenarios that pose special challenges.
Next, you will find several things to bear in mind when it comes to start working on a new module. (You will probably find some of the standards suggested by Puppet Labs).
- As we have already mentioned, it is vital to have a clear idea of the purpose of the module.
- The module should have a single entry point, customized so it is user friendly.
- Each class within a module should be in charge of performing only one task.
- It is advisable that all classes of the module inherit all variables and default values from a single class ‘params.pp’. From this single class, all variables and default values will be inherited for the ‘out-of-the-box’ operation of the module.
- Under no circumstance variables that cannot be affected by the parameters of a class should be set within such class.
Developing a module
Here you have 4 classes of a module to set and install the SSH daemon:
class sshd::install inherit sshd:params { package{'openssh-server': ensure => $sshd::version } } class sshd::service inherit sshd:params { $ensure = $sshd::start ? {true => running, default => stopped} service{"sshd": ensure => $ensure, enable => $sshd::enable, } } class sshd::config inherit sshd:params { $sshdservers = $sshd::servers file {'/etc/ssh/sshd.conf': content => template('ssh/sshd.conf.erb'); owner => root, group => root, mode => 644, } } class sshd::params { $version = "present" $servers = ["server1.domain.net", "server2.domain.net"] $enable = true $start = true }
Note how each and every one of these classes have only one task within the module. However, there is no appreciable relationship between the classes; there is neither order nor notifications in them. You should also take a look at the $sshd::version and $sshd::start variables among others. These variables belong to the ‘sshd’ class (init.pp), which is the initial entry point of the module. Check this class below:
# == Class: sshd # # A basic module to manage SSHD # # === Parameters # [*version*] # The package version to install # # [*sshdservers*] # An array of NTP servers to use on this node # # [*enable*] # Should the service be enabled during boot time? # # [*start*] # Should the service be started by Puppet class sshd inherit sshd:params( $version = $sshd:params::version, $sshdservers = $sshd:params::servers, $enable = $sshd:params::enable, $start = $sshd:params::start ) { class{'sshd::install': } -> class{'sshd::config': } ~> class{'sshd::service': } -> Class["sshd"] }
As you can see, all the variables the rest of the classes depend on are declared here; however, the default values are not defined in this class but in the sshd::params class from which all other classes inherit all variables and default values. These variables can be accessed from each class they inherit from ‘sshd::params’.
It is also worth highlighting that, if necessary, the order in which other classes will be applied is orchestrated in the ‘sshd’ class.
A bit of help
Using what we have discussed so far, we can build a fully functional module that meets the aforementioned conditions. Yet, there is an issue, every custom value that we need to set these values requires that we modify the manifest from the recipe we are applying and, if the value varies for ‘n’ number of hosts, we will have to modify the value ‘n’ times.
Here is where Hiera enters the picture, a configuration tool based on key/value queries.
Hiera uses several backend components outside the manifest of the Puppet recipe, such as YAMLs or JSONs, which can be prioritized to take one value or another depending on several conditions.
# server1.yaml ssh::server: 192.168.1.1 # server2.yaml ssh::server: 192.168.1.2 ssh:version: 2.1 # common.yaml ssh::enable: true ssh::start: true ssh::version: present
These configurations generate values according to the host in which the Puppet recipe is being applied. For instance: for ‘server1’, the ‘ssh:server’ variable will have the value ‘192.168.1.1’ and all the values from common.yaml. For ‘server2’, the behavior will be similar, but the ‘ssh::version’ variable will have the value ‘2.1’ instead of the ‘present’ value of the common.yaml
Now we can apply Hiera to our sshd::params class in order to overwrite the default values:
class sshd::params { $version = hiera('ssh::version',"present") $servers = hiera('ssh::server',["server1.domain.net", "server2.domain.net"]) $enable = hiera('ssh::enable',true) $start = hiera('ssh::start',true) }
With these changes, when it comes the time to set the values of each variable, Puppet will try to populate them with the values provided by the Hiera YAMLs and, if it doesn’t find the value in Hiera, it will apply the default value set there.
Lastly, I’d like to point out that there are several details that were not discussed in this post, but can be found in the Puppet Style Guide, and you can also check Puppet Lint, a tool that will help you validate that your module meets the standards. And, above all, do not reinvent the wheel, check first if there already is a module that meets your requirements in Puppet Forge.