While the example you provided using node-level and class variables works, it creates fragile dependencies between classes. The do_stuff
class implicitly depends on variables being set in specific parent scopes. This becomes problematic when:
- Modules need to be reused across different environments
- Multiple teams work on the same codebase
- You need to override specific parameters for testing
Parameterized classes provide explicit interfaces for your modules. Here's how we'd rewrite your example:
class bar (
String $file_owner = 'larry',
String $file_name = 'larry.txt'
) {
class { 'do_stuff':
file_owner => $file_owner,
file_name => $file_name,
}
}
class do_stuff (
String $file_name,
String $file_owner
) {
file { $file_name:
ensure => file,
owner => $file_owner,
}
}
node 'foo.com' {
class { 'bar':
file_owner => 'custom_owner',
file_name => 'special_file.txt',
}
}
From my experience managing Puppet infrastructure at scale, parameterized classes offer:
Explicit Dependencies
No more guessing where variables come from. The interface is clearly defined in the class declaration.
Default Value Flexibility
You can set sane defaults while still allowing overrides:
class nginx (
Integer $worker_processes = $facts['processors']['count'],
Boolean $gzip = true,
Array[String] $index_files = ['index.html', 'index.htm']
) {
# class implementation
}
Data Separation
Works beautifully with Hiera:
# hiera.yaml
nginx::worker_processes: 4
nginx::gzip: false
# manifest
include nginx
Type Validation
Puppet's data types provide built-in validation:
class mysql (
Enum['5.5','5.6','5.7'] $version,
Variant[String,Array[String]] $bind_address,
Optional[String] $root_password = undef
) {
# ...
}
Class Composition
Parameterized classes enable clean component assembly:
class webserver (
Boolean $ssl = true,
String $cert_source = 'puppet:///modules/certs'
) {
class { 'nginx': }
if $ssl {
class { 'letsencrypt':
email => 'admin@example.com',
}
}
file { '/etc/nginx/ssl':
source => $cert_source,
}
}
Simple utility classes that don't need configuration might not benefit from parameters. Also avoid when:
- The class has no configurable behavior
- Parameters would only be used once in the codebase
- You're working with very old Puppet versions (pre-3.0)
In your example, you're relying on dynamic scope resolution where variables declared in node definitions or parent classes become available to included classes. While this works, it creates several issues:
# Problematic example with dynamic scoping
node 'foo.com' {
$file_owner = "larry"
include bar
}
class bar {
$file_name = "larry.txt"
include do_stuff
}
class do_stuff {
file { $file_name:
ensure => file,
owner => $file_owner, # This depends on node-level variable
}
}
Parameterized classes provide explicit interfaces for your modules, making dependencies clear:
# Better approach with parameterized classes
class do_stuff (
String $file_name,
String $file_owner
) {
file { $file_name:
ensure => file,
owner => $file_owner,
}
}
class bar (
String $file_owner,
String $file_name = "larry.txt"
) {
class { 'do_stuff':
file_name => $file_name,
file_owner => $file_owner,
}
}
node 'foo.com' {
class { 'bar':
file_owner => "larry",
}
}
1. Explicit Dependencies: Parameters clearly declare what a class needs to function
2. Default Values: You can provide fallback values while allowing overrides
3. Data Type Validation: Puppet will validate parameters match expected types
4. Module Reusability: Parameterized classes work consistently across environments
Here's how we structure complex modules using parameterized classes and Hiera:
# Module interface class
class myapp (
String $version,
Optional[String] $custom_config = undef,
Array[String] $features = [],
Boolean $enable_monitoring = true
) {
# Input validation
if $version !~ /^[0-9]+\.[0-9]+$/ {
fail("Invalid version format: ${version}")
}
# Resource defaults
File {
owner => 'myapp',
group => 'myapp',
mode => '0644',
}
# Conditional logic based on parameters
if 'advanced' in $features {
include myapp::advanced
}
# Main resources
package { 'myapp':
ensure => $version,
}
file { '/etc/myapp.conf':
content => template('myapp/config.erb'),
}
}
# In Hiera YAML:
myapp::version: '2.4'
myapp::features:
- 'advanced'
- 'logging'
Parameterized classes aren't always the answer. Simple utility classes or cases where all configuration comes via facts/external data might work better with traditional includes:
# Appropriate for simple cases
class myapp::utils {
file { '/usr/local/bin/myapp-helper':
source => 'puppet:///modules/myapp/helper.sh',
mode => '0755',
}
}
# Usage:
include myapp::utils