In this episode we add the database behind Puppet: PuppetDB, which stores facts, catalogs, and reports, querying via CLI and API, plus exported resources for building cross-node monitoring and load balancers.

In episode 8 you saw how catalogs are compiled and applied. But all that information — facts, catalogs, and per-run reports — just flows by and disappears. In episode 9 we turn it into an asset with PuppetDB, the database that stores all run data and unlocks cross-node querying, including exported resources that let one node "deposit" configuration for other nodes.
PuppetDB turns Puppet from a mere config applier into a source of truth about infrastructure conditions in real time.
PuppetDB stores three main types of data:
| Data | Contents | Example use |
|---|---|---|
| Facts | All facts sent by agents each run | Find nodes with a specific OS |
| Catalogs | The last compiled catalog per node | Trace the resources a node has |
| Reports | Run results: changed, failed, skipped | Check configuration change status |
Every datum has a timestamp, so you can see history, not just the latest state. Nodes that haven't checked in for a long time will show up via stale facts.
Integration starts by enabling storeconfigs on the puppetserver and pointing it at PuppetDB:
sudo puppet config set storeconfigs true --section master
sudo puppet config set storeconfigs_backend puppetdb --section masterserver: puppetdb.example.com
port: 8081Note
PuppetDB communicates with the server over HTTPS port 8081 and needs a certificate signed by the same CA. Make sure the server CA is trusted by PuppetDB, otherwise the connection will fail.
The most convenient way to ask questions is the puppet query command, available through the puppetdb plugin:
puppet query "nodes[certname] { facts.os.family = 'RedHat' }"
puppet query "facts[certname, value] { name = 'memorysize_mb' }"
puppet query "resources[certname, title] { type = 'File' and title = '/etc/motd' }"Puppet Query Language (PQL) resembles SQL: entities like nodes, facts, and resources, columns selected in square brackets, then a filter inside the block. The first example returns the certname of all RedHat-based nodes.
Queries can also combine and, or, and not logic, plus partial matching with ~:
puppet query "nodes[certname] { certname ~ 'web' and facts.os.name = 'Ubuntu' }"Behind puppet query there's a REST API at the /pdb/query/v4 endpoint. You can call it directly with curl — useful for integrating with other tools:
curl -s -X POST https://puppetdb.example.com/pdb/query/v4 \
-H "Content-Type: application/json" \
-d '{"query": "nodes[certname] { deactivated = null }"}'The response is returned as a JSON array:
[
{
"certname": "web01.example.com",
"deactivated": null,
"catalog_timestamp": "2026-08-03T09:12:00.000Z",
"facts_timestamp": "2026-08-03T09:10:00.000Z",
"report_timestamp": "2026-08-03T09:12:00.000Z"
}
]API endpoints are grouped by entity: /pdb/query/v4/nodes, /pdb/query/v4/facts, /pdb/query/v4/resources, /pdb/query/v4/reports, and /pdb/query/v4/events. They all support the same query format.
Tip
Use puppet query for interactive exploration and the direct API when building automation. Both access the same data, so choose whichever is most convenient for each context.
Exported resources are a feature that lets one node declare resources for other nodes to use. In manifests, an exported resource is marked with two @ characters:
@@nagios_host { $facts['networking']['fqdn']:
address => $facts['networking']['ip'],
target => "/etc/nagios/conf.d/${facts['networking']['fqdn']}.cfg",
}When a web node runs, resources with @@ aren't applied on that node directly. Instead, its catalog is stored to PuppetDB — complete with the accompanying facts.
Exported resources are then collected on other nodes with the <<| |>> collector:
Nagios_host <<| |>>All nagios_host resources from every node will be applied to this node. Filtering by tag or facts narrows the collection:
File <<| tag == 'load-balancer-backend' |>>Warning
A collector waits until the exported resources are actually registered in PuppetDB. On a new node's first run, the collecting node may find nothing yet — run a second time for stable results.
The most concrete example of exported resources is a load balancer that always knows its backends. Each app node exports its own configuration:
@@file { "/etc/haproxy/backends/${facts['networking']['fqdn']}.cfg":
ensure => file,
content => "server ${facts['networking']['fqdn']} ${facts['networking']['ip']}:8080 check\n",
tag => 'haproxy-backend',
}While the load balancer node collects them:
class profile::haproxy {
package { 'haproxy': ensure => installed }
file { '/etc/haproxy/backends':
ensure => directory,
recurse => true,
purge => true,
}
File <<| tag == 'haproxy-backend' |>>
service { 'haproxy':
ensure => running,
enable => true,
subscribe => File['/etc/haproxy/backends'],
}
}When a new app node appears, it automatically exports its backend, PuppetDB records it, and on the next run the load balancer node picks up the new configuration — without a single manual intervention. Once a node is shut down and its old catalog expires, its backend disappears from the collection too.
Note
The combination of recurse => true and purge => true on the backends directory ensures config files from nodes that no longer exist are cleaned up too, preventing the load balancer from targeting dead machines.
PuppetDB turns run data into a source of truth that can be used across nodes.
puppet query provides an SQL-like query language for finding nodes, facts, and resources./pdb/query/v4 can be called directly by external tools.@@ + the <<| |>> collector distribute configuration across nodes automatically.In episode 10, you'll move up to the commercial product: Puppet Enterprise, getting to know the Console for node management, node groups and the classifier, RBAC, reporting, and running Puppet Tasks and Plans for ad-hoc actions orchestrated from a single place. See you there!