What I Am

Life as a Puppet catalogintermediate · standard · comprehensive | model: claude-opus-4-8 | 2026-06-22
Quick ref
puppet agent -t trigger a run (test mode, verbose)
--noop simulate; report drift, change nothing
puppet catalog compile build the catalog for a node
facter -p show facts (incl. custom/external)
puppet resource service inspect live state as resources
require => declare an ordering edge

What I Am

A specification, not a script

I am a compiled, node-specific graph of resources describing desired state. I am not executed top to bottom — I am converged toward. Run me twice and the second run changes nothing.

Per-node and immutable

I am built fresh for one agent at one moment, frozen from its facts and your code. The agent enforces me; the master/compiler authored me. I carry no logic — only declared intent and explicit ordering.

Note The declarative model has consequences: ordering must be stated, convergence is automatic, and "how" lives in providers, not in your manifests.

The Resource Type System

I am made of resources: typed, named declarations of desired state. Each has a type, a title, and attributes.

package { 'nginx':
  ensure => installed,
}

service { 'nginx':
  ensure  => running,
  enable  => true,
  require => Package['nginx'],
}

file { '/etc/nginx/nginx.conf':
  ensure  => file,
  content => template('nginx/nginx.conf.erb'),
  notify  => Service['nginx'],
}
Type['title']
A resource reference — unique key used to build edges in my graph.
ensure => present|absent|...
The most common attribute, expressing existence or version.
define mymod::thing { ... }
A defined type — a reusable bundle of resources you can declare many times.
Warning A given resource (type + title) may only be declared once per catalog. Duplicate declarations are a compile-time error, not a merge.

The Provider Abstraction

I describe state; providers know how to reach it. The type defines the contract; the provider implements it per platform, chosen from facts.

TypeProvider examplesSelection
packageapt, yum, dnf, gem, pipdefaulted by os.family; override with provider =>
servicesystemd, init, launchdauto-detected from init system
file(single posix/windows)by kernel
Tip Because providers abstract "how", the same manifest converges identical state on Debian and RHEL — you declare ensure => installed, not apt-get install.

Manifest & Module Structure

Your code is organised into modules; I am assembled from the classes they declare.

mymodule/
├── manifests/
│   ├── init.pp        # class mymodule { }
│   ├── config.pp      # class mymodule::config { }
│   └── service.pp     # class mymodule::service { }
├── templates/         # .epp / .erb
├── files/             # static files served to agents
├── hiera.yaml         # module-level data
├── data/              # default parameter data
└── manifests/install.pp
include mymodule
Declare a class idempotently — safe to include from many places.
class { 'mymodule': port => 8080 }
Resource-like declaration with parameters — may appear only once.
contain mymodule::config
Pull a class inside the container so relationships to the parent hold transitively.

The Compilation Pipeline

How I come to exist, from agent request to finished graph.

StageWhat happens
1. Node classificationThe compiler decides which classes apply (ENC, site.pp node blocks, or Hiera classification).
2. Fact injectionThe agent's facts arrive as top-scope $facts hash, parameterising evaluation.
3. Code evaluationManifests run as a program — conditionals, functions, Hiera lookups, template rendering — producing resources.
4. Relationship resolutionMetaparameters and chaining arrows become edges; containment is flattened.
5. Graph outputI emerge: a validated DAG of resources, serialized and sent to the agent.
Note Evaluation is imperative; the output is declarative. All your if/case logic resolves at compile time — by the time I reach the agent, every decision is already made.

Facter & the Trust Boundary

Facter gathers system facts on the agent and ships them to the compiler before I am built.

$facts['os']['family']
Structured core fact, the modern access pattern.
$trusted['certname']
Authenticated data derived from the agent's SSL cert — cannot be spoofed.
facter -p custom_fact
Test custom/external facts (facts.d/, Ruby facts) the agent will send.
Warning Facts originate on the agent and are self-reported — treat them as untrusted input. For security-relevant decisions use $trusted, which the master validates against the certificate.

Relationships, Ordering & Refresh

Order is never implied by file position — you declare every edge. I am a DAG, and the agent topologically sorts me.

MetaparameterMeaningRefresh?
before / requireThis applies before / after the target.No
notifyApply before target and send it a refresh on change.Yes (sends)
subscribeApply after source and refresh when source changes.Yes (receives)
# chaining arrows express the same edges
Package['nginx'] -> File['/etc/nginx/nginx.conf'] ~> Service['nginx']
# -> ordering,  ~> ordering + refresh