The foundation of the Puppet language: manifest structure, the anatomy of a resource type with title, parameters, and properties, seven core resource types, the declarative and idempotent concepts, and local testing with puppet apply.

In the previous episode 3, you successfully built your Puppet lab — the server and agent are connected via SSL certificates, and the first agent run went smoothly. However, the applied catalog is still empty because you haven't written anything yet. Episode 4 is when we fill that catalog: we'll learn writing manifests with resource types — the foundation of the Puppet language you'll use every day.
A manifest is a Puppet file with the .pp extension that contains configuration code. This is where you declare resources. A manifest can be applied directly to a local machine with puppet apply, or compiled by the server into a catalog for remote nodes.
A manifest contains one or more resource declarations. Here's a minimal manifest example:
package { 'curl':
ensure => installed,
}
file { '/etc/motd':
ensure => file,
content => 'Welcome to my managed server',
}Notice the pattern: you declare the end state, not the steps. This is a fundamental difference from scripting.
Every resource declaration has three parts:
package, service, or file.package { 'nginx':
ensure => '1.18.0',
}In the example above, package is the type, nginx is the title, and ensure => '1.18.0' is a parameter. Some parameters are called properties — parameters that describe the desired state and are checked by the agent, for example ensure, content, mode. Non-property parameters only control how the resource is created, for example before, require, or provider.
| Part | Example | Role |
|---|---|---|
| Type | package | System element abstraction |
| Title | 'nginx' | Unique resource identity |
| Property | ensure => running | State that is checked & converged |
| Parameter | require => Package['nginx'] | Controls ordering and behavior |
These seven resource types are the ones you'll use most often:
Manages packages through the package manager (apt, dnf, yum).
package { 'nginx':
ensure => installed,
}The ensure value can be installed/present (installed), absent (removed), latest (always the newest version), or a specific version number.
Manages systemd services (or init).
service { 'nginx':
ensure => running,
enable => true,
}ensure => running makes sure the service is up; enable => true makes sure it starts at boot.
Manages files and directories — content, ownership, and permissions.
file { '/etc/nginx/nginx.conf':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
source => 'puppet:///modules/nginx/nginx.conf',
}Runs external commands — for things no other resource can handle. Note the unless guard to stay idempotent.
exec { 'download-release':
command => 'wget -O /tmp/app.tar.gz https://example.com/app.tar.gz',
path => ['/usr/bin', '/bin'],
unless => 'test -f /tmp/app.tar.gz',
}Manages system accounts and groups.
group { 'app':
ensure => present,
}
user { 'app':
ensure => present,
gid => 'app',
shell => '/usr/sbin/nologin',
managehome => false,
}Manages cron jobs.
cron { 'backup-daily':
command => '/usr/local/bin/backup.sh',
minute => 0,
hour => 2,
}Note
There are about two dozen built-in Puppet resource types — including mount, exec, host, selinux, and notify. You can view the full documentation for any type with puppet describe package or puppet describe file.
Puppet's declarative nature means: you declare the outcome, and Puppet decides how to achieve it. From this property comes idempotency — running a manifest repeatedly produces the same end state, with no side effects from repetition.
puppet apply nginx.pp
puppet apply nginx.pp
puppet apply nginx.ppThe first run makes changes; the second and third runs only verify that the state is already correct — that's why the agent is safe to run on a 30-minute schedule.
Important
Idempotency is a contract between the manifest author and the system. The exec resource is the exception that most often breaks this contract — without a guard like unless or onlyif, exec will run its command on every agent run. Use exec only when no other resource can do the job, and always give it a guard.
Before a resource goes out to other nodes, test it locally first. puppet apply applies a manifest directly to the machine you're on — the fastest way to validate syntax and behavior.
package { 'nginx':
ensure => installed,
}
service { 'nginx':
ensure => running,
enable => true,
require => Package['nginx'],
}sudo puppet apply nginx.ppNotice the require => Package['nginx'] relationship on the service — this parameter ensures the package is installed before the service is managed. This is an example of ordering, which we'll go deeper into when we cover classes and modules.
Tip
Use puppet apply --noop for a dry run: Puppet will show the changes it would make without actually applying them. Combining --noop with --verbose is very useful for inspecting what would change before executing.
In this episode 4 you mastered the foundation of the Puppet language: understanding manifest structure, dissecting the anatomy of a resource declaration with type, title, and parameters, getting to know the seven core resource types (package, service, file, exec, user, group, cron), and testing manifests locally with puppet apply.
Key takeaways:
.pp manifest is the home of configuration; resource declarations are its content.unless/onlyif) so it doesn't break idempotency.puppet apply --noop is the go-to dry run before applying.In the next episode, episode 5, we'll level up: classes, modules & roles/profiles — organizing manifests into reusable classes, understanding the full module structure, and applying the Roles/Profiles pattern that's the industry standard for keeping the class hierarchy clean. Get your PDK ready, because the next episode is packed with hands-on work!