Puppet Tutorial: Learn Infrastructure Automation from Scratch (2026)
Puppet was the first configuration management tool I used at scale, managing over 4000 nodes across three data centers. Its declarative language and robust reporting gave me confidence that every server was configured correctly. Puppet uses a model-driven approach: you write manifests describing the desired state, Puppet compiles them into catalogs, and the Puppet agent on each node applies the catalog. With its mature ecosystem — Puppet Server, PuppetDB, Hiera, and a vast module library — Puppet remains a powerful choice for organizations that need battle-tested configuration management. This tutorial covers Puppet from the basics of manifests to advanced patterns with Hiera and roles-and-profiles.
Puppet Architecture and Installation
Puppet follows a client-server architecture. The Puppet Server runs as a Java application (using Puppet Server, the replacement for the original Ruby-based server) and compiles catalogs for nodes. The Puppet agent runs on each managed node, typically every 30 minutes, and requests its catalog. PuppetDB stores facts, catalogs, and reports, providing a queryable data store for infrastructure intelligence. I install the Puppet Server on a dedicated node with adequate CPU and memory — it is the most performance-sensitive component. Agent installation uses official packages from the Puppet repositories. The puppetserver gem manages the service. After installation, agents must sign certificates with the server — I use autosigning configured with CSR attributes for automated provisioning workflows.
# On Puppet Server
sudo apt install puppetserver
sudo systemctl start puppetserver
sudo /opt/puppetlabs/bin/puppetserver ca setup
# On Agent
sudo apt install puppet-agent
sudo /opt/puppetlabs/bin/puppet config set server puppet.example.com
sudo /opt/puppetlabs/bin/puppet ssl bootstrap
Puppet Language: Resources, Classes, and Manifests
The Puppet language is declarative. You declare resources — file, package, service, user, exec — with attributes and a state. Each resource has a type, title, and a set of parameters. Classes group related resources together. Modules organize classes, templates, files, and facts into reusable components. A manifest (.pp file) is any file containing Puppet DSL code. The site manifest (site.pp) defines the mapping from nodes to classes. I follow the principle of idempotency: every resource declaration describes the desired end state, and Puppet handles the convergence. Resource dependencies are declared with before, require, notify, and subscribe metaparameters. This ensures the correct order — for example, the package is installed before the service is started.
# /etc/puppetlabs/code/environments/production/manifests/site.pp
node default {
include nginx
include myapp
}
# /etc/puppetlabs/code/environments/production/modules/nginx/manifests/init.pp
class nginx {
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'],
}
}
Facts, Hiera, and Data Separation
Facts are key-value data about each node collected by Facter, Puppet's system inventory tool. Facts include operating system, IP address, memory, CPU count, virtualization platform. I use custom facts to expose application-level information. Hiera is Puppet's hierarchical data lookup system. It separates configuration data from code: you define values in YAML or JSON files organized by hierarchy levels — node name, environment, OS family — and Puppet looks them up automatically. The hierarchy is configured in hiera.yaml. I define a hierarchy of common defaults, environment-specific overrides, and node-specific overrides. The lookup function retrieves values, with automatic merging for hash data types. This separation means you can change configuration without modifying Puppet code.
# hiera.yaml
---
version: 5
hierarchy:
- name: "Node-specific"
path: "nodes/%{trusted.certname}.yaml"
- name: "Environment-specific"
path: "env/%{environment}.yaml"
- name: "Common defaults"
path: "common.yaml"
data_provider: hiera
# common.yaml
nginx::worker_processes: 4
myapp::port: 3000
# Puppet manifest using Hiera lookup
$port = lookup('myapp::port', Integer)
Modules and the Forge Ecosystem
Puppet Forge is the public registry of pre-built modules covering everything from Apache and MySQL to Docker and Kubernetes. The puppet module tool installs modules with dependency resolution. I use community modules as foundations and wrap them with my own profiles. The Puppet module skeleton has a standard layout: manifests, files, templates, lib (for custom facts and functions), spec (for tests), and metadata.json. Module metadata specifies dependencies, supported OS, and version. When writing modules, I follow the naming convention: module::class defines the class path. Testing modules with rspec-puppet validates compilation, and Beaker runs integration tests against real VM instances. Published modules follow semantic versioning, and the Puppetfile locks versions for environment reproducibility.
# Puppetfile
forge "https://forge.puppet.com"
mod 'puppetlabs/stdlib', '9.1.0'
mod 'puppetlabs/nginx', '5.0.1'
mod 'puppetlabs/firewall', '6.1.0'
mod 'myuser/myapp',
:git => 'https://git.example.com/puppet/myapp.git',
:ref => 'v2.3.1'
Roles and Profiles Pattern
The roles and profiles pattern is the most important design pattern in Puppet. A profile is a cohesive set of Puppet classes that configure a single technology or capability — the webserver profile installs Nginx, configures TLS, sets up logging. A role composes multiple profiles to describe an entire server's function — the application-server role includes the webserver profile, the app profile, and the monitoring profile. This abstraction decouples technology configuration from business logic. When a new monitoring agent is rolled out, I update the monitoring profile, and every role that includes it picks up the change. The role file is pure composition with maybe a variable or two. The profile is where the technical implementation lives. This pattern has saved me countless hours of refactoring.
# profile/manifests/webserver.pp
class profile::webserver {
class { 'nginx':
worker_processes => lookup('nginx::worker_processes'),
}
include profile::tls
include profile::logging
}
# role/manifests/app_server.pp
class role::app_server {
include profile::webserver
include profile::app
include profile::monitoring
}
# site.pp
node 'web-01.example.com' {
include role::app_server
}
Reporting, PuppetDB, and Compliance
Every Puppet run generates a report with status, resource changes, and performance metrics. Reports are stored in PuppetDB, a PostgreSQL-backed database that provides a query API via Puppet Query Language (PQL). I use PQL to find all nodes where a specific resource failed, check the last run time across an environment, or query nodes with a specific fact value. The Puppet dashboard (either the PE console or open-source alternatives) visualizes run status and history. For compliance, Puppet's reporting shows exactly what changed during each run — useful for audits. The enforce mode corrects drift automatically, while the noop mode reports what would change without applying it. I run noop on new nodes first, review the changes, then switch to enforce mode. This gives confidence before applying changes.
# PQL queries against PuppetDB
puppet query "nodes { certname \"web-*\" }"
puppet query "reports { latest_report_status = \"failed\" }"
puppet query "facts { name = \"osfamily\" and value = \"RedHat\" }"
# Run agent in noop mode
puppet agent --test --noop
# View last report
puppet agent --test --lastrunreport
Frequently Asked Questions
What is the difference between Puppet and Chef?
Both are configuration management tools. Puppet uses its own declarative DSL and follows a master-agent model. Chef uses Ruby DSL with a more imperative feel. Puppet has stronger built-in reporting with PuppetDB. Chef's community is stronger around cloud and CI/CD integration. I choose Puppet when reporting and compliance are top priorities.
Do I need Puppet Enterprise or is open source sufficient?
Puppet open source (now Puppet Community) provides core configuration management for free. Puppet Enterprise adds a web UI, RBAC, node management, scheduled reports, and support. For small to medium environments, the community version is sufficient. For enterprise deployments with compliance requirements and non-technical stakeholders, the PE console is valuable.
How does Puppet handle node classification?
Node classification assigns classes to nodes. External Node Classifiers (ENCs) are scripts that return node data including environment, classes, and parameters. Puppet Server supports LDAP-based, console-based (PE), and Hiera-based classification. The simplest approach is site.pp with regex node definitions. For dynamic environments, I use an ENC script that queries a CMDB or cloud inventory API.
What is the difference between Puppet's apply and agent modes?
puppet apply compiles and applies a manifest locally without a server — useful for testing and ad-hoc changes. puppet agent runs in client-server mode: the agent requests a compiled catalog from the Puppet Server and applies it. Agent mode is used for regular production runs and provides centralized reporting and PuppetDB integration.
Originally published on Ayodhyyya. Last updated June 1, 2026.