
Configuration Management 101: Writing Puppet Manifests
Configuration management is one of those things that sounds boring until your production server goes down at 3 AM and you realize you have no idea what packages were installed or how that critical service was configured. Puppet manifests are declarative scripts that define the desired state of your infrastructure, automatically ensuring your servers stay configured exactly how you want them. Whether you’re managing two servers or two thousand, understanding how to write effective Puppet manifests will save you countless hours of manual server maintenance and help you avoid those dreaded “works on my machine” situations.
How Puppet Manifests Work
Puppet uses a declarative language where you describe what you want your system to look like, not how to get there. The Puppet agent reads your manifests, compares the current system state with your desired state, and makes the necessary changes to bring them into alignment.
The core components include:
- Resources – The fundamental units like files, packages, services, and users
- Classes – Collections of resources that can be reused across nodes
- Modules – Organized collections of manifests, templates, and files
- Nodes – Individual servers or systems being managed
Every Puppet run follows the same cycle: compile the catalog from your manifests, compare it against the current system state, apply changes, and report results. This idempotent approach means you can run the same manifest multiple times safely.
Step-by-Step Implementation Guide
Let’s start with a basic manifest that installs and configures Apache. Create a file called apache.pp
:
class apache {
package { 'apache2':
ensure => installed,
}
service { 'apache2':
ensure => running,
enable => true,
require => Package['apache2'],
}
file { '/var/www/html/index.html':
ensure => file,
content => 'Hello from Puppet!
',
owner => 'www-data',
group => 'www-data',
mode => '0644',
require => Package['apache2'],
}
}
include apache
To apply this manifest locally, run:
sudo puppet apply apache.pp
Now let’s build something more realistic. Here’s a complete LAMP stack setup:
class lamp_stack {
# MySQL installation and configuration
package { ['mysql-server', 'mysql-client']:
ensure => installed,
}
service { 'mysql':
ensure => running,
enable => true,
require => Package['mysql-server'],
}
# PHP installation
package { ['php', 'php-mysql', 'libapache2-mod-php']:
ensure => installed,
}
# Apache configuration
package { 'apache2':
ensure => installed,
}
service { 'apache2':
ensure => running,
enable => true,
require => Package['apache2'],
subscribe => File['/etc/apache2/sites-available/000-default.conf'],
}
file { '/etc/apache2/sites-available/000-default.conf':
ensure => file,
content => template('lamp/apache-vhost.erb'),
owner => 'root',
group => 'root',
mode => '0644',
require => Package['apache2'],
notify => Service['apache2'],
}
# Create a PHP info page for testing
file { '/var/www/html/info.php':
ensure => file,
content => "",
owner => 'www-data',
group => 'www-data',
mode => '0644',
}
}
include lamp_stack
For better organization, create a proper module structure:
mkdir -p /etc/puppet/modules/webserver/{manifests,templates,files}
Then move your class definition to /etc/puppet/modules/webserver/manifests/init.pp
and reference it in your main site manifest.
Real-World Examples and Use Cases
Here’s a practical example for managing user accounts across multiple servers:
class user_management {
$developers = [
'alice',
'bob',
'charlie'
]
$developers.each |String $username| {
user { $username:
ensure => present,
home => "/home/${username}",
shell => '/bin/bash',
managehome => true,
groups => ['sudo', 'docker'],
}
file { "/home/${username}/.ssh":
ensure => directory,
owner => $username,
group => $username,
mode => '0700',
require => User[$username],
}
file { "/home/${username}/.ssh/authorized_keys":
ensure => file,
owner => $username,
group => $username,
mode => '0600',
source => "puppet:///modules/user_management/keys/${username}_key.pub",
require => File["/home/${username}/.ssh"],
}
}
}
Another common scenario is managing configuration files across environments. Here’s how to handle different configurations for development, staging, and production:
class database_config {
case $::environment {
'development': {
$db_host = 'localhost'
$db_pool_size = 5
$debug_mode = true
}
'staging': {
$db_host = 'staging-db.company.com'
$db_pool_size = 10
$debug_mode = true
}
'production': {
$db_host = 'prod-db.company.com'
$db_pool_size = 50
$debug_mode = false
}
default: {
fail("Unknown environment: ${::environment}")
}
}
file { '/etc/myapp/database.conf':
ensure => file,
content => epp('database_config/database.conf.epp', {
'host' => $db_host,
'pool_size' => $db_pool_size,
'debug' => $debug_mode,
}),
owner => 'myapp',
group => 'myapp',
mode => '0640',
}
}
Comparison with Alternatives
Feature | Puppet | Ansible | Chef | SaltStack |
---|---|---|---|---|
Learning Curve | Moderate (DSL) | Easy (YAML) | Steep (Ruby) | Moderate (Python) |
Agent Required | Yes | No (SSH) | Yes | Yes |
Declarative | Yes | Yes | Imperative | Both |
Idempotent | Yes | Yes | Yes | Yes |
Performance (1000+ nodes) | Excellent | Good | Excellent | Excellent |
Windows Support | Good | Good | Good | Limited |
Puppet shines in large-scale environments where you need robust configuration drift detection and reporting. The agent-based architecture provides better performance for frequent configuration checks compared to Ansible’s agentless approach, though it requires more initial setup.
Best Practices and Common Pitfalls
Here are the essential practices that will save you headaches down the road:
- Always use version control – Keep your manifests in Git and use branching strategies for different environments
- Test in isolation – Use
puppet parser validate
andpuppet-lint
to catch syntax errors early - Follow the roles and profiles pattern – Create business logic profiles that include technology-focused modules
- Use Hiera for data separation – Keep configuration data separate from your code
- Implement proper dependency management – Use
require
,before
,notify
, andsubscribe
appropriately
Common mistakes to avoid:
- Hardcoding values – Use variables and Hiera instead of embedding configuration directly in manifests
- Ignoring resource relationships – Services that depend on packages need explicit relationships
- Not testing changes – Always test manifests in a development environment first
- Overly complex conditionals – Keep logic simple and move complex decisions to Hiera or external node classifiers
- Poor module organization – Follow Puppet’s standard module structure for maintainability
Here’s an example of a well-structured profile following best practices:
class profiles::webserver {
# Data from Hiera
$packages = lookup('profiles::webserver::packages')
$document_root = lookup('profiles::webserver::document_root')
$max_workers = lookup('profiles::webserver::max_workers')
# Include base modules
include ::apache
include ::php
# Configure Apache with our specific requirements
class { '::apache::mod::php': }
apache::vhost { $::fqdn:
port => 80,
docroot => $document_root,
serveradmin => 'admin@company.com',
override => 'All',
options => ['Indexes', 'FollowSymLinks'],
apache_version => '2.4',
}
# Ensure packages are installed
ensure_packages($packages)
# Custom configuration file
file { '/etc/apache2/conf-available/custom.conf':
ensure => file,
content => epp('profiles/apache-custom.conf.epp', {
'max_workers' => $max_workers,
}),
notify => Service['apache2'],
}
}
For debugging issues, use these helpful commands:
# Dry run to see what would change
puppet apply --noop manifest.pp
# Verbose output for troubleshooting
puppet apply --verbose --debug manifest.pp
# Check syntax without applying
puppet parser validate manifest.pp
# Test specific resources
puppet resource service apache2
Security considerations include never storing passwords or secrets directly in manifests. Use Puppet’s encrypted backends like eyaml or integrate with external secret management systems. Always run Puppet agents with minimal required privileges and regularly audit your manifests for potential security issues.
The official Puppet documentation provides comprehensive coverage of advanced topics, and the Puppet Forge offers thousands of community modules to jumpstart your infrastructure automation journey.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.