This section contains a set of recommendations surrounding good module and class design. Bear in mind that Puppet development is, in principle, just like any other type of software development, and we've learned over many years in software development, and especially at O&O software, that certain modular and class design principles make our development better. I also feel that part of our journey toward infrastructure as code is making our Puppet code just as well-designed, structured, and tested as any other application code.
There's a certain class-naming convention that has developed over time within the Puppet community, and it's really worth taking these into account when structuring your classes:
init.pp
:init.pp
contains the class named the same as the module, and is the main entry point for the module.params.pp
: Theparams.pp
pattern (more on this later in the chapter) is an elegant little hack, taking advantage of Puppet's class inheritance behavior. Any of the other classes in the module inherit from theparams
class, so have their parameters set appropriately.install.pp
: The resources related to installing the software should be placed in aninstall
class. The install class must be named<modulename>::install
and must be located in theinstall.pp
file.config.pp
: The resources related to configuring the installed software should be placed in aconfig
class. Theconfig
class must be named<modulename>::config
and must be located in theconfig.pp
file.service.pp
: The resources related to managing the service for the software should be placed in aservice
class. The service class must be named<modulename>::service
and must be located in theservice.pp
file.
For software that is configured in a client/server style, see the following:
<modulename>::client::install
and<modulename>::server::install
would be the class names for theinstall.pp
file placed in theclient
andserver
directories accordingly<modulename>::client::config
and<modulename>::server::install
would be the class names for theconfig.pp
file placed in theclient
andserver
directories accordingly<modulename>::client::service
and<modulename>::server::service
would be the class names for theservice.pp
files placed in theclient
andserver
directories accordingly
init.pp
should be the single entry point for the module. In this way, someone reviewing the documentation in particular, as well as the code in init.pp
, can have a complete overview of the module's behavior.
If you've used encapsulation effectively and used descriptive class names, you can get a very good sense just by looking at init.pp
of how the module actually manages the software.
Note
Modules that have configurable parameters should be configurable in a single way and in this single place. The only exception to this would be, for example, a module such as the Apache module, where one or more virtual directories are also configurable.
Ideally, you can use your module with a simple include statement, as follows:
include mymodule
You can also use it with the use of a class declaration, as follows:
class {'mymodule': myparam => false, }
The Apache virtual directory style of configuring a number of defined types would be the third way to use your new module:
mymodule::mydefine {‘define1': myotherparam => false, }
The anti-pattern to this recommendation would be to have a number of classes other than init.pp
and your defined types with parameters expecting to be set.
As far as possible, Puppet modules should be made up of classes with a single responsibility. In software engineering, we call this high, functional cohesion. Cohesion in software engineering is the degree to which the elements of a certain module belong together. Try to make each class have a single responsibility, and don't arbitrarily mix together unrelated functionalities in your classes.
As far as possible, these classes should use encapsulation to hide the implementation details from the user; for example, users of your module don't need to be aware of individual resource names. In software engineering, we call this encapsulation. For example, in a config
class, we can use several resources, but the user doesn't need to know all about them. Rather, they just simply know that they should use the config
class for the configuration of the software to work correctly.
Having classes contain other classes can be very useful, especially in larger modules where you want to improve code readability. You can move chunks of functionality into separate files, and then use the contain keyword to refer to these separated chunks of functionality.
Note
See https://puppet.com/docs/puppet/5.3/lang_containment.html website for a reminder about the contain keyword.
If the vast majority of the people using your module will use the module with a certain parameter set, then of course it makes sense to set that parameter with a default.
Carefully think through how your module is used, and put yourself in the position of a nonexpert user of your own module.
Present the available module parameters in a sensible order, with more often accessed settings before least accessed settings, as opposed to some arbitrary order, such as alphabetical order.
In versions of Puppet proper to the new language features which came out in version 4, we would create class
parameters with undefined data types, and then, if we were being very nice, we would use the stdlib validate_<datatype>
functions to check appropriate values for those variables:
class vhost ( $servername, $serveraliases, $port ) { ...
Puppet 4 and 5 have an in-built way of defining the data type that a parameterized class accepts. See the following example:
class vhost ( String $servername, Array $serveraliases, Integer $port ) { ...