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.

In general, files must have the same named as the class or definition that it contains, and classes must be named after their function. The one exception to this rule is the main class of a module, which is defined in the init.pp 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 the init.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.

To contain classes, use the 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, because puppetlabs_spec_helper provides default Rake tasks that allow you to standardize testing across modules. It also provides some code to connect rspec-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,
    }
  }
}