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.
-
beaker-rspec
- An acceptance and integration testing framework. It provisions one or more virtual machines on various hypervisors (such as Vagrant) and then checks the result of applying your module in a realistic environment. To learn more, see the beaker-spec project.
-
serverspec
- Provides additional testing constructs (such as
be_running
andbe_installed
) forbeaker-rspec
. Serverspec allows you to test against different distributions by executing test commands locally. To learn more, see the Serverspec site.
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 if named init.pp
. This class is the module's main
interface point with Puppet, and if
possible, you should make it the only parameterized class in your module. Limiting the
parameterized classes to the main class only means that you only have to include a single
class to control usage of the entire module. This class should provide 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 should contain 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
The
resources related to configuring the installed software should be placed 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
The
remaining service resources, and anything else related to the running state of the software,
should be contained 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,
}
}
}