DEV Community

Martin Alfke for betadots

Posted on

Puppet best practice

At betadots, during our Puppet 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

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 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:

Consider adopting PDK - Puppet Development Kit or leveraging onceover.

Ensure you're using the right version of RSpec-Puppet, distinguishing between maintained projects by Rodjek and Puppet Inc..

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 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 from Voxpupuli Puppet community.

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. 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 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 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 and Voxpupuli - enhances code quality and reliability.


Examples

Below are examples illustrating various Puppet practices.

environment.conf

config_version = 'bin/config_script.sh $environmentpath $environment'
modulepath = site:modules:$basemodulepath
Enter fullscreen mode Exit fullscreen mode

bin/config_script.sh

#!/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
Enter fullscreen mode Exit fullscreen mode

manifests/site.pp

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 {}
Enter fullscreen mode Exit fullscreen mode

Hiera Config

---
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"
Enter fullscreen mode Exit fullscreen mode

Simple Hiera Data

# 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: '!'
Enter fullscreen mode Exit fullscreen mode

Module Classes

# @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
}

...
Enter fullscreen mode Exit fullscreen mode

Class Spec Tests

# 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
Enter fullscreen mode Exit fullscreen mode

Happy puppetizing,

Martin

Top comments (1)

Collapse
 
neeloj_ profile image
Neeloj P

Thank you so much, @tuxmea. I'm new to Hiera, but your explanations have made it much clearer for me. The examples are excellent, and I appreciate the mix of detailed explanations and practical examples.