Beginner's guide to writing modules
Create great Puppet modules by following best practices and guidelines.
This guide is intended to provide an approachable introduction to module best practices. Before you begin, we recommend that you are familiar enough with Puppet that you have a basic understanding of the language, you know what constitutes a class, and you understand the basic module structure.
Defining your module
Before you begin writing your module, define what it will do. Defining the range of your module's work helps you create concise modules that are easy to work with. A good module has only one area of responsibility. For example, the module addresses installing MySQL, but it doesn't install other programs or services that require MySQL.
Ideally, a module manages a single piece of software from installation through setup, configuration, and service management. When you plan your module, consider what task your module will accomplish and what functions it requires in your Puppet environment. Many users have 200 or more modules in an environment, so simple is better. For more complex needs, create multiple modules. Having many small, focused modules promotes code reuse and turns modules into building blocks.
For example, the puppetlabs-puppetdb
module deals solely
with the the setup, configuration, and management of PuppetDB. However, PuppetDB stores its data in a PostgreSQL database. Instead of trying to manage PostgreSQL with the puppetdb
module, we included the puppetlabs-postgresql
module as a
dependency. This way, the puppetdb
module can use the
postgresql
module's classes and resources to build out the
right configuration.
Class design
A good module is made up of small, self-contained classes that each do only one thing. Classes within a module are similar to functions in programming, using parameters to perform related steps that create a coherent whole.
init.p
p file, but
is called by the same name as the module. Generally, a module includes: -
The
<MODULE>
class: The main class of the module shares the name of the module and is defined in theinit.pp
file. -
The
install
class: Contains all of the resources related to installing the software that the module manages. -
The
config
class: Contains resources related to configuring the installed software. -
The
service
class: Contains service resources, as well as anything else related to the running state of the software.
For more information and an example of this structure and the code contained in classes, see the topic about module classes.
Parameters
Parameters form the public API of your module. They are the most important interface you expose, so be sure to balance to the number and variety of parameters so that users can customize their interactions with the module.
Name your parameters in a consistent thing_property
pattern, such as package_ensure
. Consistency in names helps
users understand your parameters and aids in troubleshooting and collaborative development.
If you have a parameter that manages the entire installation of a package, you can use the
package_manage
convention. The package_manage
pattern allows you to wrap all of the resources in an if $package_manage {}
test, as shown in this ntp
example:
class ntp::install {
if $ntp::package_manage {
package { $ntp::package_name:
ensure => $ntp::package_ensure,
}
}
}
To make sure users can customize your module as needed, add parameters. Do not hardcode data in your module, because this makes it inflexible and harder to use in even slightly different circumstances. For the same reason, avoid adding parameters that allow users to override templates. When you allow template overrides, users can override your template with a custom template containing additional hardcoded parameters. Instead, it's better to add flexible, user configurable parameters as needed.
For an example of a module that offers many parameters to increase flexibility, see the puppetlabs-apache module.
Ordering
Base all order-related dependencies (such as require
and
before
) on classes rather than resources. Class-based
ordering allows you to isolate the implementation details of each class. For example, rather
than specifiying require
for several packages, you can use
one class dependency. This allows you to make adjustments to the module::install
class only, instead of adjusting multiple class manifests:
file { 'configuration':
ensure => present,
require => Class['module::install'],
}
Containment
Ensure that your main classes explicitly contain any subordinate classes they declare.
Classes do not automatically contain the classes they declare, because classes can be
declared in several places via include
and similar
functions. If your classes contain the subordinate classes, it makes it easier for other
modules to form ordering relationships with your module.
contain
function. For example,
the puppetlabs-ntp
module uses containment in the main
ntp
class:contain ntp::install
contain ntp::config
contain ntp::service
Class['ntp::install']
-> Class['ntp::config']
~> Class['ntp::service']
For more information about containment, see the containment
documentation. Dependencies
If your module's functionality depends on another module, list these dependencies in the
module and include them directly in the module's main class with an include
statement. This ensures that the dependency is included in the catalog.
List the dependency to the module's metadata.json
file and
the .fixtures.yml
file used for RSpec unit testing.
Testing modules
Test your module to make sure that it works in a variety of conditions and that its options and parameters work together. PDK includes tools for validating and running unit tests on your module, including RSpec, RSpec Puppet, and Puppet Spec Helper.
Write unit tests to verify that your module works as intended in a variety of
circumstances. For example, to ensure that the module works in different operating systems,
write tests that call the osfamily
fact to verify that the
package and service exist in the catalog for each operating system your module supports.
To learn more about how to write unit tests, see the RSpec testing tutorial. For more information on testing tools, see the tools list below.
-
rspec-puppet
-
Extends the RSpec testing framework to understand and work with Puppet catalogs, the artifact it specializes in testing. This allows you to write tests that verify that your module works as intended. This tool is included in PDK.
For example, you can call facts, such as
osfamily
, with RSpec, iterating over a list of operating systems to make sure that the package and service exist in the catalog for every operating system your module supports.To learn more about
rspec-puppet
use and unit testing, see the rspec-puppet page. -
puppetlabs_spec_helper
-
Automates some of the tasks required to test modules. This is especially useful in conjunction with
rspec-puppet
, becausepuppetlabs_spec_helper
provides default Rake tasks that allow you to standardize testing across modules. It also provides some code to connectrspec-puppet
with modules. This tool is included in PDK.To learn more, see the puppetlabs_spec_helper project.
- litmus
-
Allows you to provision test platforms such as containers/images, install a Puppet agent, install a module and run tests against your systems. For more information, see the puppet_litmus documentation.
Documenting your module
Document your module's use cases, usage examples, and parameter details with README.md
and REFERENCE.md
files.
In the README, explain why and how users would use your module, and provide usage examples.
Use Puppet Strings to create the REFERENCE, which is a detailed
list of information about your module's classes, defined types, functions, tasks, task
plans, and resource types and providers. For more about writing your README and creating the
REFERENCE, see our module documentation guide
and the Strings documentation.
Versioning your module
Whenever you make changes to your module, update the version number. Version your module semantically to help users understand the level of changes in your updated module. To learn more about the specific rules of semantic versioning, see the semantic versioning specification.
After you've decided on the new version number, adjust the version number in the metadata.json
file. This allows you to create a list of
dependencies in the `metadata.json` file of your modules with specific versions of dependent
modules, which ensures your module isn't used with an old dependency that won't work.
Versioning also enables workflow management by allowing you to easily use different versions
of modules in different environments.
Releasing your module
Publish your modules on the Forge to share your modules with other Puppet users. Sharing modules allows other users to not only download and use your module to solve their infrastructure problems, but also to contribute their own improvements to your modules. Sharing modules fosters community among Puppet users, and helps improve the quality of modules available to everyone. To learn how to publish your modules to the Forge, see the module publishing documentation.
Module classes
A typical module contains a main module class, as well as
classes for managing installation, configuration, and the running state of the managed software.
The puppetlabs-ntp
module
provides examples of the classes in such a module structure.
module
The main class of any module shares the name of the module, but
the file itself is named init.pp
. This class is the module's main interface point with Puppet. If possible, make the main class the only parameterized
class in your module. Limiting the parameterized classes to only the main class means that
you only have to include a single class to control usage of the entire module. This class
provides sensible defaults so that a user can get going by just declaring the main class
with include module
.
For instance, the main ntp
class in the puppetlabs-ntp
module is the only parameterized class in the
module:
class ntp (
Boolean $broadcastclient,
Stdlib::Absolutepath $config,
Optional[Stdlib::Absolutepath] $config_dir,
String $config_file_mode,
Optional[String] $config_epp,
Optional[String] $config_template,
Boolean $disable_auth,
Boolean $disable_dhclient,
Boolean $disable_kernel,
Boolean $disable_monitor,
Optional[Array[String]] $fudge,
Stdlib::Absolutepath $driftfile,
...
module::install
The install class must be located in the install.pp
file. It contains all of the
resources related to getting the software that the module manages onto the node. The install
class must be named module::install
. In the puppetlabs-ntp
module, this class is private, which means users do not interact
with the class directly.
class ntp::install {
if $ntp::package_manage {
package { $ntp::package_name:
ensure => $ntp::package_ensure,
}
}
}
module::config
Place the resources related to configuring the installed
software in a config class. The config class must be named module::config
and must be located in the config.pp
file. In the puppetlabs-ntp
module, this class
is private, which means users do not interact with the class directly.
class ntp::config {
# The servers-netconfig file overrides NTP config on SLES 12, interfering with our configuration.
if $facts['operatingsystem'] == 'SLES' and $facts['operatingsystemmajrelease'] == '12' {
file { '/var/run/ntp/servers-netconfig':
ensure => 'absent'
}
}
if $ntp::keys_enable {
case $ntp::config_dir {
'/', '/etc', undef: {}
default: {
file { $ntp::config_dir:
ensure => directory,
owner => 0,
group => 0,
mode => '0775',
recurse => false,
}
}
}
file { $ntp::keys_file:
ensure => file,
owner => 0,
group => 0,
mode => '0644',
content => epp('ntp/keys.epp'),
}
}
...
module::service
Put the remaining service resources, and anything else related
to the running state of the software in the service class. The service class must be named
module::service
and must
be located in the service.pp
file. In the puppetlabs-ntp
module, this class is private, which means users do not interact with the class
directly.
class ntp::service {
if ! ($ntp::service_ensure in [ 'running', 'stopped' ]) {
fail('service_ensure parameter must be running or stopped')
}
if $ntp::service_manage == true {
service { 'ntp':
ensure => $ntp::service_ensure,
enable => $ntp::service_enable,
name => $ntp::service_name,
provider => $ntp::service_provider,
hasstatus => true,
hasrestart => true,
}
}
}