Puppet is a configuration-management system that enforces a declared desired state on servers, on a schedule, forever. You describe what a node should look like - packages installed, files present with given content, services running - and the Puppet agent reconciles the machine to that description on every run, correcting any drift it finds. Unlike a one-shot provisioning script, Puppet keeps running (by default every 30 minutes), so a config someone changed by hand at 2am is quietly reverted by the next agent run. For fleets of data-plane hosts - Kafka brokers, Spark workers, database nodes - that continuous reconciliation is what keeps a hundred machines genuinely identical.

Resources and the declarative model

The unit of Puppet code is a resource: a typed declaration of one piece of state. Puppet figures out the actions needed to reach it, so you never write "if not installed, install" - you declare the end state and the provider handles the rest. Resources are grouped into classes and parameterized for reuse across roles.

class profile::kafka_broker (
  String $version = '3.7.0',
  Integer $heap_gb = 6,
) {
  package { 'kafka':
    ensure => $version,
  }

  file { '/etc/kafka/server.properties':
    ensure  => file,
    content => template('profile/server.properties.erb'),
    require => Package['kafka'],
    notify  => Service['kafka'],
  }

  service { 'kafka':
    ensure => running,
    enable => true,
  }
}

The require and notify metaparameters declare ordering: the config file can't be written until the package exists, and changing the file restarts the service. Puppet builds a dependency graph from these edges, so order of declaration in the file does not dictate order of execution.

Agent runs and catalog compilation

A Puppet-managed node runs an agent that contacts the primary server, sends facts about itself (OS, memory, IP, custom facts), and receives a compiled catalog - the desired state resolved specifically for that node. The agent applies the catalog, reports what changed, and exits until the next interval. Because the catalog is compiled per node from shared code plus that node's facts, one codebase scales to a heterogeneous fleet.

# Assign roles to nodes in site.pp
node /^kafka-broker-\d+\.prod/ {
  include profile::base
  include profile::monitoring
  class { 'profile::kafka_broker':
    heap_gb => 12,
  }
}

# Dry-run on a node to preview drift without applying
# puppet agent --test --noop

The --noop flag compiles and diffs the catalog against the live machine but applies nothing - a safe way to see exactly what Puppet would change before letting it.

Where Puppet fits

Puppet earns its place on long-lived servers that need to stay in a known state over months: stateful data infrastructure, on-prem fleets, hosts with compliance baselines that must be continuously enforced. Where immutable infrastructure rebuilds the whole machine for any change, Puppet converges a mutable machine toward the target - a different model with its own trade-offs. It pairs with provisioning tools that stand up the box, leaving Puppet to own ongoing state; see Chef for a close cousin, Ansible for an agentless alternative, and the infrastructure-as-code and deployment hub for the wider set.

Frequently asked questions

Puppet or Ansible? Puppet uses pull-based agents that reconcile continuously, which suits long-lived fleets that must resist drift. Ansible is push-based and agentless, which suits orchestration and ad-hoc runs. Many shops run both - Ansible to provision and orchestrate, Puppet to hold steady state.

What does idempotent mean here? Applying the same catalog twice produces no extra changes on the second run. Puppet resources are idempotent by design: a file already matching its declared content reports no change, so an agent run on a correct machine is a no-op.

What is the difference between a class and a profile? A class is the language construct. "Profile" and "role" are a naming convention (the roles-and-profiles pattern): a profile wraps and configures component modules for one technology, and a role composes profiles into a complete machine type. Nodes are assigned exactly one role.

How do I preview changes safely? Run puppet agent --test --noop. It compiles the catalog and reports every resource it would change, with diffs for file content, but applies nothing. Drop the flag once the diff looks right.