6 min to read
How to apply roles in Puppet with Hiera
One Node to rule them all, One Node to find them, One Node to bring them all and with the Hiera bind them.

Since the introduction of Hiera, Puppet Labs have been encouraging the community to embrace separation of data and code. This enables code to become much more portable as the only thing that should change from node to node is the data that makes each unique, not the common code that configures them.
Traditionally, which roles are assigned to nodes is done in the site manifest (site.pp
). This works just fine, but it’s getting back to writing (a small amount of) Puppet code for each node as special snowflakes and is also forcing us to go to two sources of truth to know what’s happening on a node. Why not then take the seperation of data and code one step further and just do everything in Hiera?
Table of Contents
Our System
First up, this guide assumes you’re running Puppet 7. Both Community and Enterprise edition should be fine, but we’re only going to be testing with the Community edition which is free and open source. Puppet Enterprise has its own way of assigning classes to nodes through the web console, so if you’re on the enterprise version, YMMV.
Everything we’re doing here is running on Rocky Linux 8. This should run fine on your favorite flavor of Linuxy goodness though so long as it supports Puppet Server and Puppet Agent.
This article assumes you’re using the official Puppet Control Repository.
For simplicity’s sake, we’re also going to pick on just the example node and role which comes out of the box with the control repo. It’s laid out roughly like this:
.
├── CODEOWNERS
├── LICENSE
├── Puppetfile
├── README.md
├── data
│ ├── common.yaml
│ └── nodes
│ └── example-node.yaml
├── environment.conf
├── hiera.yaml
├── manifests
│ └── site.pp
├── scripts
│ ├── config_version-r10k.rb
│ ├── config_version-rugged.rb
│ └── config_version.sh
└── site-modules
├── profile
│ └── manifests
│ └── example.pp
└── role
└── manifests
└── example.pp
Lastly, we’re going to be using BASH. There are plenty of other shells available with their own pros and cons, but I guarantee on almost any major distro BASH is either going to be the default shell, or at least installed by default. If you’re not using BASH, that’s fine too, you may need to adjust the scripts a little to work around any BASHisms that are in there.
The Process
1. Update site.pp
Once you’ve checked your control repo out, launched a terminal, and changed directory to the repo’s root, run the following script to configure site.pp. What we’re doing here is setting up a dynamic include (include $class
) which takes a feed of all classes defined by a key named classes in Hiera.
if [ $(grep -E '^\$classes = lookup\(' && false) ]; then
cat <<'EOF' >manifests/site.pp
# WARNING: ALL CLASSES APPLIED VIA HIERA
# APPLY CLASSES BY NODE HERE AT YOUR OWN RISK
$classes = lookup('classes', Optional[Array[String]], 'first', undef)
if $classes {
# Include each class found.
$classes.each |$class| {
include $class
}
}
EOF
fi
The script itself is making use of a heredoc - one of my favorite BASH features - to roll out the config. The if
statement should catch any accidental attempt to run this script twice, as you don’t want to repeatedly add this block to the bottom of your site.
The lookup statement checks Hiera during a Puppet Run to find an array called classes, which we’re expecting to contain the role that the running node is meant to apply. It’s set up as an array as sometimes there will be a need or want to apply a standalone class to just one node, usually for testing. I like to err on the side of caution with things like this and build rules which are able to be subverted later down the line if there’s a need to do something weird for an edge case.
If the lookup fails, it returns undef, a special value that allows a variable to be set as undefined. In practice, what this allows us to do is test whether there are any classes defined in Hiera for a given node at all. If not, the Puppet run will skip past the whole dynamic include section and do nothing for this run (assumning you don’t have any other specific per-node config further up in site.pp.
If the lookup succeeds, it loops through each element of the array found in Hiera and includes it, executing any resulting Puppet code just like you’d included it by hand. This means that any class, profile, or role we put in the classes array in Hiera will be automatically included, meaning no more need for a bulky site.pp of doom.
2. Apply roles to nodes
From here, we’re ready to start using hiera to apply roles. Assuming your example node’s hiera is empty, you can just overwrite it with a here doc like the example below.
cat <<'EOF' >data/nodes/example-node.yaml
---
classes:
- role::example
EOF
The classes array replaces the include statements you would have previously assigned node by node in site.pp. The advantage of this is now you’ve seperated node role assignment completely from code, and almost everything can be assigned from hiera. This will take effect from the next time you deploy your control repository to the Puppet Server.
While you can declare this array in Hiera under data/common.yaml, try to avoid this anti-pattern. Your fallthrough role will become the defacto for everything, and depending on how you’ve configured arrays to merge may unintentionally apply two roles to a node causing you grief down the line.
TLDR
Here’s the quick version for the time poor Puppeteer:
if [ $(grep -E '^\$classes = lookup\(' && false) ]; then
cat <<'EOF' >manifests/site.pp
# WARNING: ALL CLASSES APPLIED VIA HIERA
# APPLY CLASSES BY NODE HERE AT YOUR OWN RISK
$classes = lookup('classes', Optional[Array[String]], 'first', undef)
if $classes {
# Include each class found.
$classes.each |$class| {
include $class
}
}
EOF
fi
cat <<'EOF' >data/nodes/example-node.pp
---
classes:
- role::example
EOF