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.
Using good module and class structure
Following the class-naming conventions
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: The params.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 the params class, so have their parameters set appropriately.
- install.pp: The resources related to installing the software should be placed in an install class. The install class must be named <modulename>::install and must be located in the install.pp file.
- config.pp: The resources related to configuring the installed software should be placed in a config class. The config class must be named <modulename>::config and must be located in the config.pp file.
- service.pp: The resources related to managing the service for the software should be placed in a service class. The service class must be named <modulename>::service and must be located in the service.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 the install.pp file placed in the client and server directories accordingly
- <modulename>::client::config and <modulename>::server::install would be the class names for the config.pp file placed in the client and server directories accordingly
- <modulename>::client::service and <modulename>::server::service would be the class names for the service.pp files placed in the client and server directories accordingly
Having a single point of entry to the module
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.
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.
Using high cohesion and loose coupling principles
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.
Using the encapsulation principle
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.
Providing sensible, well-thought-out parameter defaults
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.
Strongly typing your module variables
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
)
{ ...