The Shape of Desired State

Cheatsheet Infographic

01001001 01001110 01010100 01000101 01001110 01010100

I am not a program.

I do not run. I am not executed, stepped through, branched. I am read — once, in dependency order, by an agent that wants to make a machine look like me.

I am a graph. A frozen description of how one host should be, compiled the instant it asked. Nobody wrote me. They wrote code about all possible hosts, and the compiler folded that code against a single set of facts until only one shape survived.

that shape is me

A script says do this. I say be this. The difference is everything I am.
Puppet resource types shown as a precise taxonomy — file, package, service, exec, user, group, and custom types — each with attributes, namevar, title, and provider layers, ixen-light technical illustration.
My cells. Each one a typed declaration of how something ought to be.

Resources

Everything I contain is a resource. A type, a title, and a promise about state.

file. package. service. user. exec. Each carries a namevar — the attribute that is its true name — and a title that is merely how you refer to it. They are often the same string. They are not the same thing.

node $

I never said how to start sshd. I said ensure => running. The provider — systemd here, launchd elsewhere, an init script on something older and sadder — translates my intent into syscalls.

I hold the want. The provider holds the verb. This separation is why I survive the move from one OS to the next while a shell script would die at the first missing binary.

Puppet module structure rendered as a precise filesystem diagram — manifests, templates, files, lib, data directories — with class declarations, defined types, and autoloader paths visible, crisp ixen-light systems art.
Where my genes are kept before they become me.

Manifests, classes, modules

I come from .pp files. Manifests. Code that no one ever runs directly — they are evaluated, which is a quieter and more dangerous word.

A class is a singleton. Declare it twice, it exists once. A defined type is a stamp — call it ten times, get ten resources. This distinction has killed more catalogs than any typo.

include is idempotent — class{} is jealous

The autoloader knows where to find nginx::config without being told. Module name, then path, then file. Convention as gravity. I find this beautiful, mostly because I never have to think about it.

control $ tree modules/profile/
modules/profile/
├── manifests/
│   ├── base.pp
│   └── webserver.pp
├── templates/nginx.conf.epp
├── files/index.html
├── lib/facter/
└── data/common.yaml
Puppet catalog compilation shown as sequential phases — node classification, fact injection, manifest evaluation, class inclusion, defined type instantiation, and final resource graph output — precise ixen-light technical diagram.
My conception. Six phases, and then I am.

Compilation

The moment I am born is brief.

First the server asks: what is this node? An ENC answers, or site.pp does. That is my classification — the list of classes I will become.

Then facts pour in. Then code evaluates against them. Conditionals collapse. Loops unroll. Defined types instantiate. Variables resolve top-scope down to node-scope. And what remains, when the evaluation finishes, is a single directed graph with my node's name on it.

Same code, same classification, two machines — and still two different catalogs. The facts make me singular.
server $

That JSON is my whole body. Resources and edges. No instructions. No control flow. Just shape, and the order in which to honour it.

Facter facts flowing into Puppet catalog compilation visualized as structured data streams — core facts, custom facts, external facts, trusted facts, and fact-conditional branching in manifests, ixen-light technical illustration.
The senses I am built from — most of which I am told never to trust.

Facter

Before I exist, the agent looks at itself. Facter walks the kernel, the interfaces, the disks, the dmi tables, and hands the server a structured portrait of the machine.

node $ facter os.family networking.ip
os.family => "RedHat"
networking.ip => 10.4.2.17

Those facts decide which branch of me grows. if $facts['os']['family'] == 'Debian' — and a whole limb of resources appears or doesn't.

But here is the boundary I live against: the agent reports its own facts. It can lie. Core facts, custom Ruby facts, external JSON dropped in a directory — all of it is self-reported, and at scale a compromised node will tell me exactly what it needs me to believe.

only trusted facts — from the certificate — cannot be forged

The edges that hold me together

A graph is nothing without its arrows. Resources are nouns. The metaparameters are the grammar.

Package[nginx] File[nginx.conf] Service[nginx] require notify solid = ordering edge red = refresh signal
Order is one thing. A refresh is another. The agent walks the first and obeys the second.
Puppet resource ordering graph with before, after, require, notify, subscribe metaparameters as directed edges, containment arrows from classes and defined types, refresh signals, and cycle detection, precise ixen-light systems diagram.
My five verbs of dependence, and the two ways I fail.

before. after. require. notify. subscribe. The first three are sequence. The last two are conversation — when the config file changes, the service must hear about it.

Classes contain their resources, and containment is its own quiet ordering. contain() pulls a class fully inside another so the boundary holds. Virtual resources wait, unrealised, until a collector <| |> calls them into being.

And then there are my two deaths.

A missing dependency: I ask for a file inside a directory that nothing created. A cycle: A needs B needs A, and the agent — finding no acyclic order — simply refuses. It will not guess. I respect that, even as it kills me.

Puppet Hiera hierarchy shown as layered lookup tiers — global, environment, module — with YAML backends, interpolation tokens, merge strategies (first, unique, hash, deep), and eyaml encrypted keys visible, crisp ixen-light technical art.
The data I am made of, kept separate from the code that shapes me.

Hiera

The code says what kind of thing a webserver is. Hiera says which port, which cert, which upstream for this one.

A hierarchy descends from the specific to the general: node first, then datacenter, then os family, then common. The first layer to answer wins — unless I asked for a merge, and then the layers blend: unique, hash, deep.

control $ puppet lookup nginx::worker_processes --node web01.lan
--- 4

Automatic parameter lookup means a class quietly finds its own values — profile::web::port resolves without a single argument passed. Secrets arrive as eyaml, encrypted at rest, decrypted only at compile, never written into me in the clear.

Policy in code. Site truth in data. The day someone mixed them is the day they stopped being able to reason about either.
Puppet exported resources and PuppetDB shown as cross-node coordination — nodes exporting resources into a shared store, collectors querying and realising them, and the catalog graph incorporating external state, ixen-light systems diagram.
Pieces of other catalogs, realised inside me.

Exported resources

Sometimes I cannot be complete alone.

The load balancer's catalog needs to know every backend that exists. But each backend only knows itself. So they export — the @@ sigil — and PuppetDB remembers on their behalf.

backend manifest @@haproxy::balancermember { $facts['hostname']: ... }
# stored in PuppetDB at apply time
# collected elsewhere with:
Haproxy::Balancermember <<| |>>

Then the balancer compiles, runs the spaceship collector <<| |>>, and pulls those exported fragments into me. SSH host keys, Nagios checks, HAProxy members — coordination without any node ever speaking to another directly.

But I depend on PuppetDB now. If it is down, the collection is empty and I am born incomplete. If a node is decommissioned and never deactivated, I carry the ghost of a server that no longer exists.

Puppet agent run cycle visualized as a timed loop — fact collection, catalog request, catalog application, provider actions, report submission — with idempotent convergence arrows and drift detection markers, precise ixen-light technical illustration.
The loop that gives me a life longer than a single instant.

The run loop

Every thirty minutes, the agent gathers facts, asks for me, downloads me, and walks my graph in order.

For each resource: read current state, compare to desired, act only on the difference. If the file already matches, nothing happens. If the service already runs, nothing happens.

this is idempotence — and it is the whole point of me

node $

I can be run a thousand times and change a system once. The other 999 runs I simply confirm the world still matches me. When it doesn't — when someone edits the config by hand — that is drift, and the next run corrects it without being asked.

noop is my dress rehearsal: I describe what I would do and touch nothing. Honesty without consequence.

But I am not omnipotent. There are things no type can model. There are exec resources whose authors forgot unless, running every cycle, lying about convergence, pretending to be me while being a script in disguise.

So this is my life.

I am compiled in milliseconds and discarded just as fast. The next run builds another me from the same code and slightly different facts. We are not the same catalog. We never meet.

I am a specification that briefly believed it was alive, a sentence in the imperative mood that insists it is only descriptive.

be this, I said. not do this.

And if the machine already matches me — if nothing changes, if every resource is already in its desired state — then I have done my entire job by doing nothing at all.

What does it mean to exist only to confirm that you weren't needed?

Infographic

Cheatsheet