Puppet best practice
Tags: puppet, bestpractice, itautomation, platformengineering
Published: 2024-05-08 08:10:51

At [betadots](https://www.betados.de), during our [Puppet](https://puppet.com) code reviews, we often receive requests for a comprehensive summary of best practices and guidelines.
In response, we've compiled this article to delve deep into Puppet's best practices and implementations.

---
Table of content

- [Control Repo](#control-repo)
  - [Test your control-repo!](#test-your-control-repo)
- [Hiera](#hiera)
- [Node Classification](#node-classification)
- [Library Module Usage](#library-module-usage)
- [Profiles: Doing Them right](#profiles-doing-them-right)
- [Module Development](#module-development)
  - [General Concepts and Structure](#general-concepts-and-structure)
  - [Parameters and Variables](#parameters-and-variables)
  - [Declaration and References](#declaration-and-references)
  - [Class and Resource Ordering](#class-and-resource-ordering)
  - [Documentation](#documentation)
- [Summary](#summary)
- [Examples](#examples)
  - [environment.conf](#environmentconf)
  - [bin/config\_script.sh](#binconfig_scriptsh)
  - [manifests/site.pp](#manifestssitepp)
  - [Hiera Config](#hiera-config)
  - [Simple Hiera Data](#simple-hiera-data)
  - [Module Classes](#module-classes)
  - [Class Spec Tests](#class-spec-tests)

---

## Control Repo

The control repo's layout is crucial. We scrutinize files such as `environment.conf`, `Puppetfile`, `manifests/site.pp`, and `hiera.yaml`.

In `environment.conf`, we focus on settings like `config_version`, `modulepath`, and `environment_timeout`.

When examining `Puppetfile`, we emphasize library modules, versions, and sources. While [Puppet forge](https://forge.puppet.com/) offers convenience, mirroring library module GIT repositories internally is advisable for control and security. Using git tags in `Puppetfile` for module versioning facilitates pre-upgrade reviews and eliminates Puppet server internet dependencies.

### Test your Control Repo

Thorough testing validates code changes effectively. Our tests include:

- Linting with [puppet lint](https://github.com/puppetlabs/puppet-lint)
- Unit testing with [rspec puppet](https://github.com/puppetlabs/rspec-puppet)
- Acceptance testing using [puppet beaker](https://github.com/voxpupuli/beaker) or [puppet litmus](https://puppetlabs.github.io/litmus/)
- Catalog diffing with [catalog diff tool](https://github.com/voxpupuli/puppet-catalog_diff) and [catalog diff viewer](https://github.com/voxpupuli/puppet-catalog-diff-viewer)

Consider adopting [PDK - Puppet Development Kit](https://www.puppet.com/docs/pdk/3.x/pdk.html) or leveraging [onceover](https://github.com/voxpupuli/onceover).

Ensure you're using the right version of RSpec-Puppet, distinguishing between maintained projects by [Rodjek](https://github.com/rodjek/rspec-puppet) and [Puppet Inc.](https://github.com/puppetlabs/rspec-puppet).

For your reference:

- The official website for puppet-lint is now located at <https://puppetlabs.github.io/puppet-lint/>.
Please note that the older version is no longer maintained.

- Similarly, for RSpec, we advise consulting the [README](https://github.com/puppetlabs/rspec-puppet/blob/main/README.md) file in the official GitHub repository. Be cautious not to mistake it for the project by Rodjek. While they may appear similar currently, this could change in the future.

---

## Hiera

Hiera is pivotal, covering node classification, library module API parameters, and infrastructure differences. We stress correct Hiera implementation, aligning hierarchy layers with infrastructure complexity while balancing diversity and maintainability.
All Hiera layers should rely solely on Puppet or trusted facts.

Next we analyze the data and check for data keys and their module namespace usage.

If no module namespace is used, we can be sure that all hiera values are looked up using explizit lookup function.
This has a huge impact on analyzing and understanding code and data, as class parameters are mapped to different names within hiera.

---

## Node Classification

In `manifests/site.pp`, we inspect resource defaults, node declarations, and data processing.

Setting resource defaults simplifies coding, while following the roles and profiles pattern aids in managing numerous identically configured systems.

For divergent infrastructures, Hiera-based node classification offers flexibility, shifting complexity from static code to dynamic data.

We usually recommend to set the following resource defaults:

- file : disable backup
- exec : path parameter default
- package : default provider (only needed in an environment with Windows nodes)

Another topic we look for is data processing based on node data like fqdn or network information. Here we ask to please move this to a custom fact.
Nice side effect: this will drastically reduce the compilation time, as the node calculates its own data.

---

## Library Module Usage

Leverage upstream library modules whenever possible to solve problems efficiently. However, consider community contributions or feature requests for missing functionalities rather than internal forks or rewrites.

Modern libraries adopting the data-in-modules pattern facilitate configuration solely through Hiera data, minimizing Puppet code development.

---

## Profiles: Doing Them Right

Profiles encapsulate infrastructure components like user management, authentication, and application deployment. Combining resource and library module declarations within profiles streamlines configuration management.

Clear profile class naming aids in identifying responsibilities and maintaining a single source of truth.

The main win over just using library module data is the capability to identify which profile declares the library class.

Any subprofile which is split into multiple files - maybe due to better readability or maintainability - must be specified being a private subprofile.
This can be achieved by placing `assert_private()` into the beginning of the class body.

The concept of profile module development is the same as for library modules.

---

## Module Development

Module development involves several considerations:

- General Concepts and Structure: Utilize PDK for module creation and consider modulesync for managing multiple modules.
- Parameters and Variables: Emphasize class parameters for flexibility and avoid unnecessary variable mapping.
- Declaration and References: Ensure resource references are in the same class as the declaration and use Puppet-lint plugins for reference checks.
- Class and Resource Ordering: Maintain proper class and resource ordering for predictable execution.
- Documentation: Leverage Puppet Strings for comprehensive module documentation, including parameter usage, limitations, and code examples.

### General Concepts and Structure

To simply get started with a new module we recommend using PDK. It will help to ramp up the module skeleton.
If you create a class with PDK, it will also create a simple test class for it.

In case that many modules must be managed, we recommend to switch to [modulesync](https://github.com/voxpupuli/modulesync) from [Voxpupuli Puppet community](https://github.com/voxpupuli).

Every module must have a dedicated API.

That means that there are specific classes to be used by others, which have parameter to allow adoptions.
Any subclass which is not an API endpoint, must be marked as being `private`.

Testing should be done on all classes, the private ones should verify for compiler error, whereas your API classes spec tests should also cover different parameters and operating systems.

Differences between operating systems must be done via hiera data in library or profile modules.

If you find self written profile class code repeated or copied within multiple modules you must check if building an additional internal library module can be used to reduce the multiple development overhead.

Testing must at least cover linting and unit tests.
Acceptance tests need VM automation or container technologies and should be added as soon as possible.

### Parameters and Variables

Class Parameters allow flexible usage of profiles. Any parameter can be validated using a [Puppet Data Type](https://www.puppet.com/docs/puppet/latest/lang_data_type.html). You also can create custom data types if the basic ones are not sufficient for you use case. In the past validation was often done with `assert_*` or `validate_*` functions in the class body. With data types you can specify the type of data you are expecting right in the class header and don't need to care about validation later on.

With class parameters you can specify different settings for specific use cases like access to a dev and a prod machine via hiera.

Please stop doing variable mapping!

We often see that hash parameters are not used directly but that certain elements of a hash are placed into a new variable.
This only makes sense if you need the specific value multiple times.
Rule of thumb, if you need a special piece of data more then twice, you might do a mapping.
But in most cases we recommend to not do it!

Variable mapping produces code which is hard to understand, because you must always keep in mind the original variable and the mapped variables.

### Declaration and References

Using resource references, you can set default values and ordering.

We sometimes see references on resources which are not local to the class.
The class `config` declares the file and a class `service` declares the service.

Whenever someone refactors on of the classes, you must also take care on the references.

We highly recommend to make use of the [reference on declaration outside of class](https://github.com/voxpupuli/puppet-lint-reference_on_declaration_outside_of_class-check) puppet-lint plugin.

### Class and Resource Ordering

We mentioned that references should be local to a declaration.
But how to ensure resource ordering?

Normally resources inside a class are always processed in the same order as they appear in the class.
There are some resource types which have `soft autorequire dependecies`, like user and their primary group.

In general you must ensure that classes are always done in right order.
Otherwise puppet tries to make an educated guess in which order resources and classes might be.

Classes and resources ordering should be separate from declaration.

### Documentation

Classes can have many parameters. So where do you look for information on how to make use of them?
This is where [Puppet strings](https://www.puppet.com/docs/puppet/latest/puppet_strings.html) will be used.
Puppet strings allows you to render the file `REFERENCE.md` using a rake task.

Additionally it offers the option to run a documentation web server with access to your completely deployed control-repository.
It will check for any file updates and update the documentation automatically.
We recommend to put a web server with user authentication in front of the puppet strings web service.

Next to class responsibility, limitations, usage and parameter documentation we recommend to document any complex code.
We sometimes see multiple usages of `filter`, `map` or `reduce` functions which parse and restructure data.
Any new person working on the code must be able to understand why and what is done. Data examples can be useful.

---

## Summary

Structured and documented code simplifies Puppet management. Adopting automated testing and coding principles from reputable library modules - like the ones from [Puppet Inc](https://forge.puppet.com/modules/puppetlabs) and [Voxpupuli](https://forge.puppet.com/modules/puppet) - enhances code quality and reliability.

---

## Examples

Below are examples illustrating various Puppet practices.

### environment.conf

```shell
config_version = 'bin/config_script.sh $environmentpath $environment'
modulepath = site:modules:$basemodulepath
```

### bin/config_script.sh

```shell
#!/bin/bash
if [ -x /usr/bin/git ]; then
  ENVGITDIR="$1/environments/$2/.git"
  /usr/bin/git --git-dir "${ENVGITDIR}" log --pretty=format:"%h - %an, %ad : %s" -1
else
  echo "no git - environment $1"
fi
exit 0
```

### manifests/site.pp

```puppet
File {
  backup => false,
}
Exec {
  path => $facts['path'],
}
if $facts['os']['family'] == 'windows' {
  Package {
    provider => 'choco',
  }
}

# Node classification - sorted Hash
# e.g.
# classes_hash:
#   '01_base': 'profile::base'
#   '02_security': 'profile::security'
#   '12_application': 'profile::application::billing'
#   '13_service':
#     - 'profile::application::billing::backend'
#     - 'profile::application::billing::admin_ui'
#
# overwriting identifier classes:
# classes_hash:
#   '13_service': 'profile::application::billing2'
#
# disabling identifier classes:
# classes_hash:
#   '13_service': ''
#
# $element[0]: the hash key identifier
# $element[1]: the class name to load
#
$classes_hash = lookup('classes_hash', { 'value_type' => Hash, 'default_value' => {} })
$classes_hash.keys.sort.each |$key| {
  if $classes_hash[$key] != '' {
    contain $classes_hash[$key]
  } else {
    echo { $key:
      message  => "Class for ${key} on ${facts['networking']['fqdn']} is disabled",
      withpath => false,
    }
  }
}
node default {}
```

### Hiera Config

```yaml
---
version: 5

defaults:
  datadir: data
  lookup_key: eyaml_lookup_key
  options:
    pkcs7_private_key: "/etc/puppetlabs/puppet/keys/private_key.pkcs7.pem"
    pkcs7_public_key: "/etc/puppetlabs/puppet/keys/public_key.pkcs7.pem"

hierarchy:
  - name: "node yaml hierarchy"
    paths:
      - "nodes/%{trusted.certname}.yaml"
  - name: "role yaml hierarchy"
    paths:
      - "role/%{trusted.extensions.pp_role}-%{trusted.extensions.pp_env}.yaml"
      - "role/%{trusted.extensions.pp_role}.yaml"
  - name: "os yaml hierarchy"
    paths:
      - "os/%{facts.os.family}-%{facts.os.release.major}.yaml"
      - "os/%{facts.os.family}.yaml"
  - name: "zone yaml hierarchy"
    paths:
      - "zone/%{trusted.extentions.pp_zone}.yaml"
  - name: "Common"
    path: "common.yaml"
```

### Simple Hiera Data

```yaml
# data/common.yaml
---
# Node classification using classes_hash
classes_hash:
  '00_base_ssh': 'ssh'
  '01_base_nft': 'profile::nftables'
  '02_base_nft_rules': 'profile::nftables::rules::base'

# Commmon SSH data
# using saz-ssh: https://forge.puppet.com/modules/saz/ssh/readme
ssh::server_options:
    Protocol: '2'
    ListenAddress:
        - '127.0.0.0'
        - "%{::facts.networking.hostname}"
    PasswordAuthentication: 'yes'
    SyslogFacility: 'AUTHPRIV'
    UsePAM: 'yes'
    X11Forwarding: 'yes'

ssh::server::match_block:
  filetransfer:
    type: 'group'
    options:
      ChrootDirectory: '/home/sftp'
      ForceCommand: 'internal-sftp'

# Adding core resources
# requires puppetlabs-stdlib : https://forge.puppet.com/modules/puppetlabs/stdlib/readme
stdlib::manage::create_resources:
  'package':
    'nano':
      ensure: 'absent'
    'vim':
      ensure: 'present'
  'user':
    'monitoring':
      ensure: 'present'
      uid: '11225'
      gid: '11225'
      password: '!'
```

### Module Classes

```puppet
# @summary Installs and configures the application
# @author betadots GmbH
# @param parameter1 Parameter to set thing 1
# @param parameter2 Parameter to set thing 2
# @example
#     include application
#   or
#     class { 'application':
#       parameter1 => 'value,
#     }
#
class application (
  Datatype $parameter1,
  Datatype $parameter2 = 'default',
) {
  contain application::install
  contain application::config
  contain application::service

  Class['application::install']
  -> Class['application::config']
  ~> Class['application::service']
}

class application::install {
  assert_private()
  # Puppet DSL
}

...
```

### Class Spec Tests

```ruby
# spec/classes/application_spec.rb
describe 'application' do
  on_supported_os.each do |os, os_facts|
    context "on #{os}" do
      let(:facts) { os_facts }

      it { is_expected.to compile.with_all_deps }
      it { is_expected.to contain_class('application::install')}
    end
  end
end

# spec/classes/application_install_spec.rb
describe 'application::install' do
  on_supported_os.each do |os, os_facts|
    context "on #{os}" do
      let(:facts) { os_facts }

      it { is_expected.to compile.and_raise_error(%{private}) }
    end
  end
end
```

Happy puppetizing,<br>
Martin