Effective Puppet Adoption for Small Teams: Overcoming Sysadmin-to-Code Transition Challenges


2 views

When traditional sysadmins first encounter Puppet's declarative DSL, it feels like being asked to write poetry in a foreign language. The key is to reframe infrastructure as "executable documentation" rather than "code". Start with simple manifests that replace shell scripts:

# Instead of:
# ssh web01 "apt-get install nginx && systemctl start nginx"

# Write this in Puppet:
package { 'nginx':
  ensure => installed,
}

service { 'nginx':
  ensure  => running,
  require => Package['nginx'],
}

The Puppet Forge contains over 5,000 modules, but quality varies. Implement this evaluation checklist:

  1. Support Score: Look for modules with >80% Puppet Supported or Puppet Approved status
  2. Activity: Last update within 6 months and regular commit history
  3. Dependencies: Fewer than 5 required modules (check metadata.json)

For example, when choosing an Apache module between puppetlabs/apache and example42/apache, the puppetlabs version wins with:

"dependencies": [
  {"name":"puppetlabs/stdlib","version_requirement":">= 4.13.1 < 7.0.0"}
]

Adopt this directory structure for transparency:

environment/
├── production/
│   ├── manifests/
│   │   └── site.pp         # Node definitions
│   ├── modules/
│   │   ├── profile/        # Business logic
│   │   ├── role/           # Server roles
│   │   └── custom_module/  # Local modules
│   └── hiera.yaml          # Configuration data

Implement the Roles and Profiles pattern:

# role/webserver.pp
class role::webserver {
  include profile::base
  include profile::nginx
  include profile::firewall
}

Create a module skeleton with pdk new module and enforce these conventions:

  • Parameter validation using Puppet's data types
  • Single class responsibility principle
  • rspec-puppet tests for all manifests

Example module structure:

custom_module/
├── manifests/
│   ├── init.pp
│   └── config.pp
├── spec/
│   ├── classes/
│   └── fixtures/
├── templates/
│   └── config.erb
└── metadata.json

These extensions are worth learning:

Tool Purpose When to Use
Hiera Separate data from code Environment-specific configurations
Augeas Edit config files Modifying existing files like /etc/ssh/sshd_config
Trocla Password management When storing credentials in Puppet

Replace "let's just run it" with this systematic approach:

# Dry-run with detailed logging
puppet apply --noop --debug manifest.pp

# Show resource dependency graph
puppet apply --graph manifest.pp
xdg-open /opt/puppetlabs/puppet/cache/state/graphs/relationships.dot

For existing infrastructure, generate Puppet code from current state:

puppet resource package
puppet resource service

Many sysadmin teams face resistance when adopting Puppet, especially when coming from traditional manual administration backgrounds. The key concerns often revolve around:

  • Perceived complexity of the DSL (Domain Specific Language)
  • Module selection paralysis in the Puppet Forge ecosystem
  • Maintainability of custom manifests
  • Keeping up with Puppet's evolving best practices

Instead of formal training, consider this incremental approach:

# Sample learning path manifest
node 'learning-path' {
  # Stage 1: Basic resource types
  include ::learn_puppet::files
  include ::learn_puppet::packages
  
  # Stage 2: Simple modules
  include ::learn_puppet::webstack
  
  # Stage 3: Advanced patterns
  include ::learn_puppet::roles_profiles
  include ::learn_puppet::hiera_integration
}

When evaluating Puppet Forge modules:

# Module evaluation checklist in Puppet code
$module_quality_checks = {
  'downloads'       => >10000,
  'puppet_version'  => '>= 5.5.0',
  'dependencies'    => <5,
  'ci_status'       => 'passing',
  'activity'        => 'updated_within(6 months)'
}

Implement these directory structures for clarity:

manifests/
├── site.pp                # Node definitions
├── roles/                 # Business logic
│   ├── webserver.pp
│   └── database.pp
└── profiles/              # Technical implementation
    ├── apache.pp
    └── mysql.pp

modules/
├── organization_custom/   # Company-specific modules
└── third_party/           # Forge modules
  • Puppet Strings: Documentation generator
    # Example documentation comment
    # @param ensure Whether to install or remove package
    # @param version Specific version to install
    define mymodule::package (
      Enum['present','absent'] $ensure = 'present',
      Optional[String] $version = undef
    ) {
      package { $title:
        ensure => $ensure,
        version => $version,
      }
    }
    
  • Onceover: Lightweight testing framework
  • PDK: Puppet Development Kit for module creation

Create a controlled adoption process:

# hiera.yaml version control example
:version: 5
:hierarchy:
  - "nodes/%{trusted.certname}"
  - "environments/%{environment}"
  - "common"
  
# Only enable extensions after evaluation
$enable_experimental_features = {
  'trocla' => false,
  'augeas' => true,
  'deferred' => true
}

For the "let's just run it" mentality:

# puppet/control-repo/manifests/phases.pp
node 'webserver01' {
  # Phase 1: Core OS configuration
  include ::phase1::baseline
  
  # Phase 2: Essential services (2 weeks later)
  if $::phase >= 2 {
    include ::phase2::webstack
  }
  
  # Phase 3: Optimization (1 month later)
  if $::phase >= 3 {
    include ::phase3::tuning
  }
}