Fattening the workflow, part 4: Roles and profiles

December 2, 2020 

If the workflow that includes the control repository, r10k and GitLab still feels too light, there are yet more ways to make the workflow heavier by adding more abstraction to the Puppet modules themselves. This is done by adopting the "Roles and Profiles pattern" that was developed by Craig Dunn and popularized in the blog posts by Gary Larizza.

Roles and profiles are just Puppet classes. Profiles are usually used for creating technology stacks. A good example is a web application whose installation requires a web server (nginx, apache), a database server (postgresql, mysql) and several other components. The configurations for such an application could of course be done with a "regular" Puppet module but the reusability of the module would be rather limited for two reasons:

  • The more moving parts, the more eventual combinations, and only a few users of the module would want the same combination.
  • If you try to force all the moving parts into the same universal module, the module will become very complicated and fragile.

This is where the profiles come in: they are not meant for universal use but mainly to create site specific complex configurations by building on top of component modules like apache, mysql and postgresql. You don't have to worry about other people when you develop something for your own use, which keeps the complexity of the class at a reasonable level. Profiles usually get their parameters with the lookup function from Hiera.

Here is an example of a simple profile that is used for configuring basic settings for a typical *NIX server:

# @summary
#   General settings for *NIX type machines in a certain
#   environment
#
class profile::unixbase
{
 
# Default email address that several modules are using
$email = lookup('default_email', String)

# Internal network and subnet mask, for example
# 10.95.5.0/24
$intranet = lookup('intranet', String)
 
# Puppet server address
$puppet_server = lookup('puppet_server', String)
 
# Classes and defined resources
include ::bash

bash::config::user { 'samuli':
  ensure => 'present',
}

include ::localbackups

class { '::locales':
  locales => ['en_US.UTF-8 UTF-8',
              'fi_FI.UTF-8 UTF-8',
              'it_IT.UTF-8 UTF-8', ],
}

include ::localusers

class { '::mdns':
  allow_ipv4_address => $intranet,
}

include ::nano

class { '::ntp':
  ensure    => 'running',
  ntp_pools => [ '0.fi.pool.ntp.org',
                 '1.fi.pool.ntp.org',
                 '2.fi.pool.ntp.org', ]
}

include ::postfix

class { '::puppetagent':
  master => $puppet_server,
}

class { '::puppetagent::cron':
  splaylimit => '10m',
  email      => $email,
}

include ::ssh

class { '::sshd':
  passwordauthentication => 'yes',
  permitrootlogin        => 'without-password',
}

include ::systemd

class { '::timezone':
  timezone => 'Europe/Helsinki',
}

class { '::updater':
  hour    => 13,
  minute  => 5,
  weekday => '*',
  install => 'yes',
  mailon  => 'error',
}

}

This profile lets you easily see on a general level which system configurations it's going to modify. It's retrieving a few "global" parameters from Hiera and uses them as class parameters. The lookups are right at the beginning of the profile so the parameters the profile uses are easily available in one single location instead of being spread up throughout the profile.

Using lookups instead of class parameters has certain benefits:

  • You can more easily reuse "global" parameters like such as IP addresses, domains and email addresses in multiple profiles. If you used parameterized classes you'd need to pass those values to each profile separately in Hiera.
  • Merging of data structures like hashes and arrays. For example you may define local user accounts in Hiera at the common level (e.g. data/common.yaml) and add more user accounts on the node level (e.g. data/nodes/server.example.org.yaml).
  • You can include the same profile several times. This is useful if the node has several roles (see below) that include the same profile. Using a parameterized profile would result in Puppet drive failing.

Roles are on an even higher level of abstraction and are used for stacking profiles only:

@summary set up a webserver
class role::webserver {
  include ::profile::unixbase
  include ::profile::nginx
}

After this you only need to add one line in the yaml file of the node to make the machine a web server:

classes:
  - role::webserver

Roles and profiles allow building very complex configuration while keeping those configurations readable and understandable.

Fattening the workflow series:

Samuli Seppänen
Samuli Seppänen
Author archive
menucross-circle