Puppet Roles and Profiles with Foreman ENC

Good day folks!

I’m looking for some advise from those out there with large established environments who have implemented the “Roles and Profiles” pattern using Foreman as the means to supply parameters. I’m working with Foreman 1.18/Katello 3.7 at $day_job, and Foreman 1.19 at home (home prod/lab), using Puppet 5 in both places. At $day_job, I am working on a reference implementation, which will include Foreman/Katello, Ansible integration for multi-node orchestration and some on-off tasks, while Puppet 5 will be used for the on-going configuration and compliance management. I’m currently implementing the roles and profiles, and need to support multiple flavors of *nix and Windows too. The git repo contains all puppet modules used, so there will be consistent point-in-time deployments of the config management code. There would be another git repo for Ansible playbooks, and I would similarly hope to leverage Foreman’s dashboard there as well for setting variables and parameters. I’m trying to get the foundations in place to move towards containers as well.

I’m struggling with where to implement parameters, especially since I’m updating some stuff from stuff that was originally developed for the Puppet Dashboard 1.3/Puppet 2 era. I want to have as much of the puppet modules (and Ansible playbooks) “fixed” in git-managed code, and expose out those parameters that may change depending on deployment environments. I want to leverage the Foreman ENC to supply the parameters, and utilize the data that already exists in the Foreman dashboard (like the DNS servers associated with a subnet). I want to also be able to set parameters by domain, location, or subnet for stuff like log servers, metrics collectors, etc. I’ve currently implemented a filter in ignored_environments.yaml to filter out all classes from the foreman dashboard except for the role and profile modules classes.

The design pattern I’m currently working towards would look like the following:

An example role, the goal of which is that each system would have one assigned role:

class role::example inherits role {

  include profile::example

}

The role class itself would include the base profile (common core services configuration and OS baseline configuration):

class role {

  include profile::base

}

Profile base (would include common dependency classes, and then pull in specific classes based on Facter data):

class profile::base (
  Array $nameservers        = $::nameservers,
  String $domain            = $::domain,
  $search                   = $::search,
  Array $ntpservers         = $::ntpservers,

) {

  # Common for all OS

  include ::stdlib

  # Include OS Specific Customization

  include "profile::${downcase($::kernel)}::base"


}

There would be directories for linux, windows, solaris, aix, etc. Each would have it’s respective OS configuration, like the linux example:

class profile::linux::base  (
  Array $nameservers        = $::nameservers,
  String $domain            = $::domain,
  $search                   = $::search,
  Array $ntpservers         = $::ntpservers,
  String $relayhost         = $::relayhost,
  String $syslog_server     = $::syslog_server,
  String $collectd_server   = $::collectd_server,
  String $puppetmaster      = $::puppetmaster

) {


  # Include Linux OS specific configuration parameters
  include "profile::linux::${downcase($::operatingsystem)}"

  # Common Includes for all Linux Systems
  include profile::linux::puppet
  include profile::linux::dnsclient
  include profile::linux::time
  include profile::linux::logging
  ...
}

The idea is that things that need to be customized for CentOS, RHEL, SuSE, Debian, Ubuntu, etc. would be customized in the down-cased $::operatingsystem file. Each core service would have some decision logic for configuring the systemd versions of core services, or the traditional ones. You’ll note that I have some nameservers and ntpservers parameters in profile::base and profile::linux::base; part of my uncertainty is where to place parameters so that operations doesn’t have to put Smart Parameter overrides all over the place in the Foreman dashboard (and people lose track of what is applied where). My hope was to put core stuff (DNS, NTP, domains, etc.) in the profile::base, and then handle OS specific configurations in their classes, but maybe I’m actually causing more sprawl that way, and should just leave all parameters in each OS specific class… And at this point, I’m still working on infrastructure level classes, and haven’t gotten to workload specific classes yet, which will be a whole different pile of fun…

So, currently, I have Smart Class parameter overrides for stuff like nameservers, which I set as ‘array’ and set the default value to ["<%= @host.subnet.dns_primary %>","<%= @host.subnet.dns_secondary %>"]. I set other things as parameters in Subnets, domains, locations, as needed for those things, because they are specific to a place/network/group/etc. I have some global parameters like ‘hostgroup_name’ being set to <%= @host.hostgroup.name %>. So there is a bit of scatter on that front as well. It’s currently handled using scripts that use the hammer_cli to set the parameters/overrides/settings, and could be documented appropriately when the design pattern is fully established.

Puppet 4 and 5 seem to handle inheritance differently than Puppet 2 & 3, and a lot of the examples out there reference hiera lookups, not pulling from Foreman parameters. Before I go too far down my proposed path (and maybe paint myself into a corner), I’m hoping someone might give me a sanity check on what I’m currently doing based on their experiences. This implementation has the potential to get big and move to a number of non-connected environments, so I’m trying to get a solid procedural foundation and class framework in place to accommodate that, and have a good flow from the engineering side to the ops side.

I would greatly appreciate any feedback you all would care to share! Thanks in advance for your time and comments!

We are using and trying to hone a similar design.

We use hiera for all our core and secure parameters so that they are recorded in Git and have a separate params module with a global params class and classes for each os in order to expose the parameters we want in foreman.

We also have a hierarchy level that uses the foreman ENC hostgroup parameter to apply hiera parameters per hostgroup.

What classes would you typically apply to a host in the foreman interface under your design to get the parameters?

Ours would be something like

role::server
params::global
params::debian

Keen to bounce ideas around.

Matt,

The approach I’ve been taking so far is to include default parameters within the puppet modules, and then any necessary overrides are applied via Foreman. I’ve been avoiding using hiera, as that represents (to me anyway) another place to put configuration information which may drift. I’ve been trying to have a similar design pattern for Puppet and Ansible, where there should be sane defaults applied, and anything that might change is applied via profile classes or smart parameter overrides. Ansible doesn’t have hiera, so I’m trying to maintain some consistency between how each automation technology is implemented, as well as maintaining consistency between *nix and Windows in the overall approach. A bit part of that is to include documentation within the puppet classes that details the requirements that are being satisfied by the class, which can be pulled out using puppet-strings or yard in the pipeline to build up the documentation package for each solution.

To answer your question on what classes would I apply on this design; currently, I’m focused on the infrastructure aspects, establishing core and common services, and feature parity across a number of operating systems. I’d have to deal with new workloads, as well as absorbing existing workloads. So, my goal is to only apply a single role to a hostgroup or system, and it pulls in everything via inheritance and includes. So, in my simplified example above, role::example has it’s workload specific profile(s) applied to add the application stuff plus any other special sauce (like user accounts, software packages, config). I hope to have it inherit the role class, which includes profile::base. profile::base includes some classes that are common to everything (like stdlib), and then it makes conditional judgments to pull in additional classes depending on facter facts. So, basically a programmatic decision tree, where all the infrastructure stuff has been figured out in sufficient detail that all the use cases are covered, and the ENC can make deterministic choices. Once I get the infrastructure side (my responsibility) solid, then I plan to offer the same stuff up to the application groups to leverage, so I’m trying to figure things out from their perspectives as well.

So, the parameters I’m looking to have it inherit (initially) are things that should be well defined in the environment, like core services. So, if configuration items have been loaded into Foreman, like the DNS servers in the Subnets definitions, I should just be able to pull that stuff in. Like setting $::nameservers = Array ["<%= @host.subnet.dns_primary %>","<%= @host.subnet.dns_secondary %>"] in the smart parameters override. I’ve started building up the classes and assigning them directly to the hostgroups, like profile::linux::base, and applying the overrides there, which works great. Now I’m trying to work up towards roles, and figuring out where I should apply parameters that would make sense for operations, while still working with Puppet and the Foreman ENC. Puppet 2 used to have issues where variable types would change between inherited classes (like array would collapse into a string that looked like the array), but we could creatively work with that. Puppet 4 and 5 are strongly typed, so the variables won’t change; it either works great, or will complain because the variable wasn’t passed between classes, and it borks. Still working through this with some trial and error, so I figured I’d ask to see what others are doing…

Since you mentioned host groups, I’d be interested to know how others use them (and other organizing constructs like Organizations). Because I’m using Katello, I need to reserve Organizations for entities that might have Red Hat subscriptions, since those are managed per organization. Plus, I may need to leverage organizations for RBAC roles as well. I need to investigate both aspects further for our use cases. Hostgroups are different because they are more functionally used, so I’ve been setting up a few example ones to work through all the in-service support aspects of using them. I currently have hierarchical hostgroups, with the parent one being the base OS configuration (CentOS 7 base). I have sub-groups for functional roles (CentOS 7 base/apache web). I’ve also created a Parent one as an example for business groups, although I’m personally not big on that approach (Client Group/system role). I’d prefer to deal with them using profiles classes that capture functional roles (i.e. profile::func_role::dba) or a less preferred approach of business entities (profile::bus_entity::dba), that would be included in the role. So, I’d be interested in hearing how others end up herding the cats within a common IT dashboard infrastructure using organizations and host groups when you have a lot of different stakeholders in the mix. We’ve got big apps like ERPs, and smaller multi-tier apps, some managed by dedicated groups, others managed by common groups.

I have avoided the use of config groups, because I feel that it’s another place to look for which classes are applied and people can be inconsistent in applying them; I’ve been preferring to just have the one role applied to a system or hostgroup, so that it’s easy for both automation and manual configuration, and the role has been assessed via the development process. Others may be using them in a way I haven’t thought of, so I’d be interested in hearing other peoples approaches to that. I know that some apply the roles and profiles pattern by assigning profile classes to a config group to become a role, but my preference is to do that in code within the role.

Like yourself, I’m also keen to bounce ideas around and discuss. Both Puppet and Foreman have a lot of flexible framework constructs, so they lend themselves to many different ways to accomplish something. I’m interested in seeing what works well for people, and what people have found to be limiting or challenging (and that’s not necessarily the technology’s fault!).

Thanks for joining the conversation!