In this episode we separate data from code with Hiera 5: layered data hierarchies, hiera.yaml, the lookup function, YAML and JSON data in modules and environments, and encrypting secrets with hiera-eyaml.

In episode 6 you learned how to read node data through facts and turn it into decisions inside manifests. But writing if and case for every possible value will fill your code with business logic. In episode 7 we move data out of the code with Hiera 5, Puppet's built-in hierarchical data system.
With Hiera, configuration is stored as pure data — YAML or JSON — looked up through a hierarchy. Manifest code just fetches data via lookup, and the same value can handle all nodes simply by changing its position in the hierarchy.
Hiera solves one problem: where a value comes from, and who has priority. Each datum is defined once in a flat file, then the hierarchy determines the order of priority. The more specific a level, the higher its priority.
version: 5
defaults:
datadir: data
data_hash: yaml_data
hierarchy:
- name: "Per-node data"
path: "nodes/%{trusted.certname}.yaml"
- name: "Per-OS family"
path: "osfamily/%{facts.os.family}.yaml"
- name: "Per-OS"
path: "os/%{facts.os.name}.yaml"
- name: "Common"
path: "common.yaml"The hierarchy is read from the top: Hiera tries nodes/<certname>.yaml first. If the key isn't found there, it moves down to osfamily, then os, and finally common.yaml.
Important
The location of hiera.yaml determines its scope. In /etc/puppetlabs/puppet/hiera.yaml the hierarchy applies to the whole environment; inside a module's data/ folder, the hierarchy only applies to that module. Environment configuration takes precedence over modules.
The most common data levels are common, osfamily, and hostname. Together they represent a healthy strategy: common values for everyone, per-OS-family adjustments, and per-node exceptions.
ntp_servers:
- 0.pool.ntp.org
- 1.pool.ntp.org
dns_servers:
- 8.8.8.8
- 1.1.1.1
timezone: UTCssh_packages:
- openssh-server
web_server_pkg: httpdtimezone: Asia/Jakarta
app_port: 8080Notice the pattern: common.yaml holds the default values, the per-OS-family file holds customizations that apply to many nodes, and the per-node file holds exceptions. Node web01.example.com gets timezone: Asia/Jakarta from the node level, not UTC from common.
Tip
The per-node file name must exactly match the certname — letter case matters too. If the certname is WEB01.EXAMPLE.COM, the Hiera file must also be WEB01.EXAMPLE.COM.yaml.
Inside manifests, Hiera data is fetched with the lookup function. The simplest form:
$ntp = lookup('ntp_servers')
$tz = lookup('timezone')lookup can be constrained by data type and given a default so it doesn't error when the key is missing:
$port = lookup('app_port', Integer, 'first', 80)
$dns = lookup('dns_servers', Array[String], 'unique')
$rules = lookup('firewall_rules', Hash, 'deep')The second parameter constrains the result type, the third is the merge strategy, and the fourth is the default value. The merge strategy determines how values from multiple levels are combined:
| Strategy | Behavior |
|---|---|
first | Use the value from the highest level that has the key |
unique | Merge all array values from all levels, removing duplicates |
hash | Merge hashes across levels, higher-level keys win |
deep | Like hash, but nested keys are merged too |
For example, lookup('ntp_servers', Array[String], 'unique') will merge the NTP server lists from common.yaml and other levels without duplicates.
Modules can also carry their own data through a data/ directory. This is useful for a module's default values, while the environment still has the right to override them:
profile::web/
data/
common.yaml
hiera.yaml
manifests/
templates/profile::web::worker_processes: 4
profile::web::listen_port: 80Notice the key names: profile::web::worker_processes follows the fully qualified name of the class parameter. Hiera then fills it in automatically as a class parameter when the class is included — this is the main bridge between data and code.
Note
To use automatic lookup for class parameters, the class must use automatic parameter binding — just include without class { ... }. Hiera data named name::module::parameter will be wired up by itself.
Secrets like passwords should never sit in plaintext in git. hiera-eyaml is a Hiera backend that stores encrypted values inside regular YAML files, while the private key is kept on the server.
Prepare the keys and encrypt a value:
sudo puppet module install puppetlabs-eyaml --modulepath /etc/puppetlabs/puppet/modules
eyaml encrypt --pkcs7-private-key=/etc/puppetlabs/eyaml/keys/private_key.pkcs7.pem \
--pkcs7-public-key=/etc/puppetlabs/eyaml/keys/public_key.pkcs7.pem \
--string "S3cr3t"The output of the command above is an ENC[PKCS7,...] block ready to be placed in the data. Example result:
db_password: ENC[PKCS7,MIIEvgYJKoZIhvcNAQcCoIIErzCCBKsCAQExAAEwDQYJKoZIhvcNAQEBBQAEggSg...]
db_user: app_readerThen register the eyaml backend in the hierarchy, usually as the highest level so secrets always win:
version: 5
defaults:
datadir: data
data_hash: yaml_data
hierarchy:
- name: "Secrets"
lookup_key: eyaml_lookup_key
path: "secrets.yaml"
options:
pkcs7_private_key: /etc/puppetlabs/eyaml/keys/private_key.pkcs7.pem
pkcs7_public_key: /etc/puppetlabs/eyaml/keys/public_key.pkcs7.pem
- name: "Common"
path: "common.yaml"In manifests, encrypted secrets are still read normally with lookup('db_password') — Puppet Server decrypts them at catalog compilation, and agents never see the private key.
Warning
Publish the public key to your team and back up the private key to a safe place off the server. If the private key is lost, every secret encrypted with that key can no longer be decrypted. Never put the private key in a repository.
With Hiera, you move all value decisions from code to centralized data.
hiera.yaml defines the priority order, and %{facts.os.family} and %{trusted.certname} make it dynamic.lookup fetches values with type control, merge strategies, and safe defaults.data/ folder, and the environment has the right to override it.Now manifests only contain "what to do", not "which value to use". In episode 8, you'll see how manifests and data come together into a catalog, learn to order resources with before, require, notify, subscribe, and the -> and ~> chaining arrows, plus the most common catalog errors. See you there!