Taking manifests to the next level: reusable classes, parametrized classes, the complete module structure, PDK scaffolding, and the Roles/Profiles pattern for keeping the class hierarchy clean.

In the previous episode 4, you wrote manifests and got to know resource types — package, service, file, exec, user, group, and cron — and tested them with puppet apply. The problem is that a single manifest won't survive as your configuration grows: you'll repeat the same resources across many files and struggle to apply them to many nodes. Episode 5 answers this problem with three concepts: classes to group resources, modules to package them, and the Roles/Profiles pattern to keep the architecture clean.
A class is a named collection of resources that can be reused. Define it once with class, then apply it anywhere with include.
class profile::nginx {
package { 'nginx':
ensure => installed,
}
service { 'nginx':
ensure => running,
enable => true,
require => Package['nginx'],
}
}To apply the class to a node, call it with include:
include profile::nginxinclude is idempotent — calling it multiple times doesn't cause resources to be declared twice, as long as all the classes called handle different resources.
Classes can accept parameters so one definition serves many variations. Parameters are given default values so they remain safe to call without arguments.
class profile::nginx (
String $listen_port = 80,
Boolean $enable_ssl = false,
) {
package { 'nginx':
ensure => installed,
}
service { 'nginx':
ensure => running,
enable => true,
require => Package['nginx'],
}
file { '/etc/nginx/conf.d/server.conf':
ensure => file,
content => "listen ${listen_port};\nssl ${enable_ssl};\n",
require => Package['nginx'],
notify => Service['nginx'],
}
}When using it, call it with the desired values:
class { 'profile::nginx':
listen_port => 8443,
enable_ssl => true,
}Note
There are two ways to call a class: include and the resource-like declaration class { 'name': }. include is recommended because it's safe to call repeatedly and flexible with Hiera data — we'll see this when data enters the picture in the Hiera episode.
Scattered manifests get hard to manage. Puppet solves this with modules — structured packages that contain manifests, templates, files, and data. Modules are Puppet's distribution and versioning unit; you can download them from Puppet Forge or write your own.
nginx/
├── manifests/
│ ├── init.pp # nginx class
│ └── service.pp # nginx::service class
├── templates/
│ └── server.conf.epp # config template
├── files/
│ └── index.html # static file
├── hieradata/
│ └── common.yaml # Hiera data
├── examples/
│ ├── init.pp
├── spec/
│ └── ... # unit tests
├── metadata.json # description & dependency
└── README.mdAn important naming rule: the init.pp file inside the nginx module defines the nginx class, and the service.pp file defines nginx::service.
PDK, which we installed in episode 0, provides automatic scaffolding:
pdk new module profile --template-url=https://github.com/puppetlabs/pdk-templates
cd profile
pdk new class nginxAs a result, the manifests/init.pp and manifests/nginx.pp files are ready to fill in. PDK also provides pdk validate and pdk test to make sure the code is valid and passes unit tests.
Tip
Run pdk validate every time you finish writing a manifest. The validator will catch syntax and style errors before they hit the server — far cheaper than fixing things after a failed agent run.
As infrastructure grows, the class hierarchy easily becomes messy — classes merge with business data, and changing one part risks breaking another. The Roles/Profiles pattern splits that layer into two:
profile::nginx or profile::postgresql. This is where all resources and technical details live.role::web or role::database. Roles contain no resources — they only include several profiles.class profile::nginx (
Integer $worker_processes = 4,
) {
package { 'nginx':
ensure => installed,
}
service { 'nginx':
ensure => running,
enable => true,
require => Package['nginx'],
}
}class role::web {
include profile::base
include profile::nginx
include profile::firewall
}Notice: role::web doesn't know how nginx is installed — it only knows that a web node needs the nginx, base, and firewall profiles. You can swap profile::nginx for another implementation without changing the role.
| Layer | Focus | Example | Contents |
|---|---|---|---|
| Profile | Technical | profile::nginx | Package, service, file, template |
| Role | Business | role::web | Only include profiles |
Important
The golden rule of Roles/Profiles: roles must not contain resources directly — only include profiles. If you find resources inside a role, that's a sign the layers are leaking. Separate them right away, because reorganizing later costs far more than doing it now.
This pattern is fully supported by node classification: a node is assigned one role, and that role brings in all the profiles it needs. We'll connect everything with Hiera and node classification in the following episodes.
In this episode 5 you leveled up from single manifests to module architecture: defining reusable classes, writing parametrized classes for flexibility, understanding the complete module structure (manifests, templates, files, hieradata), building modules via PDK, and applying the Roles/Profiles pattern that separates the technical layer from the business layer.
Key takeaways:
init.pp determines the main class name.pdk validate, and pdk test.include profiles — keep the layers clean.In the next episode, episode 6, we'll cover Hiera: data-driven configuration — separating data from logic with a hierarchical database, understanding the hiera.yaml structure, automatic class parameter lookup, and practical application for dev, staging, and production environments. See you in the next episode!