Intro to Roles and Profiles with Puppet and Hiera

If you’ve been following along with the Puppet series, our next task is to start using roles and profiles. If you’re just visiting, feel free to review the series to get caught up. Today, we will discuss the roles and profiles pattern, start implementing it as well as a custom fact, and deploy a webserver on a node managed by puppet. Finally, we’ll move some of our configuration from the site manifest into Hiera.

NOTE: A small note on security. I’ve been running through this series as ‘root’ and earlier said, “Well, just be more secure in production.” That’s lame. This blog covers security as well as virtualization and automation so I’m going to live up to that. For now, I’ve added a local user with useradd, updated sudoers, and cloned all the repos so that I can show best practices, which will include doing most work as my user and then sudo/su to run a few commands as root. Later, we’ll manage local users via puppet.

[root@puppet git]# useradd rnelson0 -c "Rob Nelson"
[root@puppet git]# passwd rnelson0
Changing password for user rnelson0.
New password:
BAD PASSWORD: it is based on a dictionary word
Retype new password:
passwd: all authentication tokens updated successfully.

[root@puppet ~]# cat > /etc/sudoers.d/puppetadmins
rnelson0        ALL=(ALL)       ALL

<Login as rnelson0>
[rnelson0@puppet ~]$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/rnelson0/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/rnelson0/.ssh/id_rsa.
Your public key has been saved in /home/rnelson0/.ssh/id_rsa.pub.
...
<Import key into github>
...
[rnelson0@puppet ~]$ cd git
[rnelson0@puppet git]$ git clone git@github.com:rnelson0/puppet-tutorial
Initialized empty Git repository in /home/rnelson0/git/puppet-tutorial/.git/
remote: Counting objects: 848, done.
remote: Compressing objects: 100% (579/579), done.
remote: Total 848 (delta 190), reused 841 (delta 186)
Receiving objects: 100% (848/848), 395.47 KiB, done.
Resolving deltas: 100% (190/190), done.
[rnelson0@puppet git]$ git clone git@github.com:rnelson0/rnelson0-base
Initialized empty Git repository in /home/rnelson0/git/rnelson0-base/.git/
remote: Reusing existing pack: 35, done.
remote: Total 35 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (35/35), 10.11 KiB, done.
Resolving deltas: 100% (10/10), done.
[rnelson0@puppet git]$ git clone git@github.com:rnelson0/site_mcollective.git
Initialized empty Git repository in /home/rnelson0/git/site_mcollective/.git/
remote: Reusing existing pack: 31, done.
remote: Total 31 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (31/31), 12.36 KiB, done.
Resolving deltas: 100% (5/5), done.

Provision Server01

At the beginning of this series, I described a set of 10 nodes we would use for learning simply called server01 through server10. If you did not already provision those nodes, you need to deploy at least one now. You can use a different name and IP but I will be referencing server01 and 10.0.0.51 throughout this article. Since it has been some time, be sure to either update your kickstart template or run ‘yum update -y‘ and reboot after deployment to avoid version mismatches and install the latest security patches. Be sure to add a local user and update sudoers as we have above so that you can manage the node securely.

Roles and Profiles

We’re going to look into implementing the roles and profile pattern. This pattern is very popular, though it is perhaps poorly named. In this pattern, you define a number of profiles, which specify resources for each profile, and then define roles that are a collection of individual profiles. Ideally, a role has many profiles, and each node definition references a single role. If a node requires more than one role, you should define a new role that has the union set of profiles the two roles have. Some people believe that each node should have a single profile that denotes the roles it should have and hence the pattern is named incorrectly. However, I will use the pattern as described, mostly because the majority of documentation on the internet assumes that you follow the one role/many profile pattern and implementing it the other way around thus leads to some confusion.

NOTE: I have documented an improved workflow since this was written. I highly suggest following the newer repository layout, though this repository layout still works.

Before we begin, create a role and a profile module. I’ve created two repos on Github (called simply role and profile), two corresponding modules on the server with puppet module generate as we did above, and pushed the new files up to Github. Here are the commands, with the output truncated:

[rnelson0@puppet rnelson0-custom_facts]$ cd ..
[rnelson0@puppet git]$ puppet module generate --modulepath `pwd` rnelson0-role
[rnelson0@puppet git]$ puppet module generate --modulepath `pwd` rnelson0-profile
[rnelson0@puppet git]$ cd rnelson0-role/
[rnelson0@puppet rnelson0-role]$ git init
[rnelson0@puppet rnelson0-role]$ git add .
[rnelson0@puppet rnelson0-role]$ git commit -m "first commit"
[rnelson0@puppet rnelson0-role]$ git remote add origin git@github.com:rnelson0/rnelson0-role.git
[rnelson0@puppet rnelson0-profile]$ git add .
[rnelson0@puppet rnelson0-profile]$ git commit -m "first commit"
[rnelson0@puppet rnelson0-profile]$ git remote add origin git@github.com:rnelson0/rnelson0-profile.git
[rnelson0@puppet rnelson0-profile]$ git push -u origin master

Let’s build two example profiles of a base profile and a very simple web server. Our web server will require nothing more than apache. We can build the base profile by looking at init.pp and grabbing the ssh and ntp settings and putting them in manifests/profile/base.pp.

[rnelson0@puppet rnelson0-profile]$ cat > manifests/base.pp
# == Class: profile::base
#
# Base profile
#
# === Parameters
#
# None
#
# === Variables
#
# None
#
# === Examples
#
#  include profile::base
#
# === Authors
#
# Rob Nelson <rnelson0@gmail.com>
#
# === Copyright
#
# Copyright 2014 Rob Nelson
#
class profile::base {
  include ::motd

  # SSH server and client
  class { '::ssh::server':
    options => {
      'PermitRootLogin'          => 'yes',
      'Protocol'                 => '2',
      'SyslogFacility'           => 'AUTHPRIV',
      'PasswordAuthentication'   => 'yes',
      'GSSAPIAuthentication'     => 'yes',
      'GSSAPICleanupCredentials' => 'yes',
      'AcceptEnv'                => 'LANG LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY LC_MESSAGES LC_PAPER LC_NAME LC_ADDRESS LC_TELEPHONE LC_MEASUREMENT LC_IDENTIFICATION LC_ALL LANGUAGE XMODIFIERS',
      'Subsystem'                => '      sftp    /usr/libexec/openssh/sftp-server',
      'Banner'                   => '/etc/issue.net',
    },
  }
  class { '::ssh::client':
    options => {
      'Host *' => {
        'SendEnv'                   => 'LANG LC_*',
        'HashKnownHosts'            => 'yes',
        'GSSAPIAuthentication'      => 'yes',
        'GSSAPIDelegateCredentials' => 'no',
      },
    },
  }

  class { '::ntp':
    servers => [ '0.pool.ntp.org', '2.centos.pool.ntp.org', '1.rhel.pool.ntp.org'],
  }
}
CTRL-D

Let’s create a simple profile for the webserver and apply the puppetlabs’s apache class. Note that we have to use the full path to the module, ::apache; if we leave off the colons then it will find apache within the profile module and create a circular reference.

[rnelson0@puppet rnelson0-profile]$ cat > manifests/apache.pp
# == Class: profile::apache
#
# Apache profile
#
# === Parameters
#
# None
#
# === Variables
#
# None
#
# === Examples
#
#  include profile::apache
#
# === Authors
#
# Rob Nelson <rnelson0@gmail.com>
#
# === Copyright
#
# Copyright 2014 Rob Nelson
#
class profile::apache {
  class {'::apache': }
}
CTRL-D

Finally, let’s create a role for the webserver called role::webserver:

[rnelson0@puppet rnelson0-profile]$ cd ../rnelson0-role/
[rnelson0@puppet rnelson0-role]$ cat > manifests/webserver.pp
# == Class: role::webserver
#
# Webserver role
#
# === Parameters
#
# None
#
# === Variables
#
# None
#
# === Examples
#
#  include role::webserver
#
# === Authors
#
# Rob Nelson <rnelson0@gmail.com>
#
# === Copyright
#
# Copyright 2014 Rob Nelson
#
class role::webserver {
  include profile::apache
  include profile::base  # All roles should have the base profile
}

The roles and profiles are going to become handy when we define a fact that we can use to match the role.

Custom Facts

What is a fact, in Puppet’s parlance? A fact is a piece of information about a puppet node that is collected by the master system, or the local system when using puppet apply. This information appears in the form of top-scope variables that can be accessed in a manifest as $fact. Some examples are osfamily (redhat, debian, etc.), timezone, is_virtual, fqdn, and network information in the format ipaddress_<int>. You can see facts on a system with puppet by running facter (builtin facts only) or facter -p (includes puppet-defined facts). You must be root to see all facts, regular users only see some facts. Add the name of a fact if you just want to see that result.

[rnelson0@puppet ~]$ facter | wc
     74     244    3061
[rnelson0@puppet ~]$ sudo facter | wc
     85     300    3416
[rnelson0@puppet ~]# facter timezone
GMT

You can use these facts in a manifest. A common use is to determine an action to be taken based on the OS family – Redhat uses .rpm, Debian uses .deb, Solaris uses .pkg, and so on. When writing modules for public consumption – and I encourage that you design with this in mind even if the module is private – it is good practice to provide multi-platform support and this is a great way to do so. You can use any of the facts this way, for instance to use the IP address for eth0 as a default value, to check the free RAM before taking an action, or detecting if the node is running on a virtual platform.

What if you want the master to know something about nodes that isn’t provided by facter? That’s where custom facts come in. We can create a fact of our own choosing by writing some Ruby code, add it to the master, and on the next checkin, agents will receive the fact and start providing it in reports. I’m going to walk us through the process, and you can find more details here.

NOTE: If you started with Puppet about the same time as I started this series, you may have v1.7.x of facter. The current version for CentOS as of this writing is 2.0.1-1. Be sure to update with “yum update -y” or your distribution’s equivalent to ensure you match the latest and greatest before continuing. If you must stay at a previous version, you may run into some issues that you’ll have to debug on your own.

Requirements

We have a few requirements to use custom facts. The first is to ensure that pluginsync is enabled on the master(s). Make sure this configuration option is set in /etc/puppet/puppet.conf, and if not, set it and restart the puppetmaster service:

[main]
pluginsync = true

We also have to comply with the module structure <modulepath>/<module>/lib/facter/<customfact>.rb. Let’s create a new module and a repo called custom_facts and add a fact called in role.rb. If you have upgraded to puppet 3.5.x, you’ll notice that puppet module generates metadata for you during generation now!

[rnelson0@puppet git]$ puppet module generate --modulepath `pwd` rnelson0-custom_facts
We need to create a metadata.json file for this module.  Please answer the
following questions; if the question is not applicable to this module, feel free
to leave it blank.

Puppet uses Semantic Versioning (semver.org) to version modules.
What version is this module?  [0.1.0]
-->

Who wrote this module?  [rnelson0]
-->

What license does this module code fall under?  [Apache 2.0]
-->

How would you describe this module in a single sentence?
--> Custom facts for roles and profiles

Where is this module's source code repository?
--> https://github.com/rnelson0/rnelson0-custom_facts

Where can others go to learn more about this module?  [https://github.com/rnelson0/rnelson0-custom_facts]
-->

Where can others go to file issues about this module?  [https://github.com/rnelson0/rnelson0-custom_facts/issues]
-->

----------------------------------------
{
  "name": "rnelson0-custom_facts",
  "version": "0.1.0",
  "author": "rnelson0",
  "summary": "Custom facts for roles and profiles",
  "license": "Apache 2.0",
  "source": "https://github.com/rnelson0/rnelson0-custom_facts",
  "project_page": "https://github.com/rnelson0/rnelson0-custom_facts",
  "issues_url": "https://github.com/rnelson0/rnelson0-custom_facts/issues",
  "dependencies": [
    {
      "name": "puppetlabs-stdlib",
      "version_range": ">= 1.0.0"
    }
  ]
}
----------------------------------------

About to generate this metadata; continue? [n/Y]
--> y

Notice: Generating module at /home/rnelson0/git/rnelson0-custom_facts...
Notice: Populating ERB templates...
Finished; module generated in rnelson0-custom_facts.
rnelson0-custom_facts/tests
rnelson0-custom_facts/tests/init.pp
rnelson0-custom_facts/Rakefile
rnelson0-custom_facts/metadata.json
rnelson0-custom_facts/spec
rnelson0-custom_facts/spec/spec_helper.rb
rnelson0-custom_facts/spec/classes
rnelson0-custom_facts/spec/classes/init_spec.rb
rnelson0-custom_facts/README.md
rnelson0-custom_facts/manifests
rnelson0-custom_facts/manifests/init.pp
[rnelson0@puppet git]$ cd rnelson0-custom_facts/
[rnelson0@puppet rnelson0-custom_facts]$ mkdir facter
[rnelson0@puppet rnelson0-custom_facts]$ touch facter/roles.rb
[rnelson0@puppet rnelson0-custom_facts]$ git init
Initialized empty Git repository in /home/rnelson0/git/rnelson0-custom_facts/.git/
[rnelson0@puppet rnelson0-custom_facts]$ git add .
[rnelson0@puppet rnelson0-custom_facts]$ git commit -m 'First commit of custom_facts module'
[master (root-commit) a6239d7] First commit of custom_facts module
 7 files changed, 191 insertions(+), 0 deletions(-)
 create mode 100644 README.md
 create mode 100644 Rakefile
 create mode 100644 facter/roles.rb
 create mode 100644 manifests/init.pp
 create mode 100644 metadata.json
 create mode 100644 spec/classes/init_spec.rb
 create mode 100644 spec/spec_helper.rb
 create mode 100644 tests/init.pp
[rnelson0@puppet rnelson0-custom_facts]$ git remote add origin git@github.com:rnelson0/rnelson0-custom_facts.git
[rnelson0@puppet rnelson0-custom_facts]$ git push -u origin master
Counting objects: 15, done.
Compressing objects: 100% (10/10), done.
Writing objects: 100% (15/15), 3.77 KiB, done.
Total 15 (delta 0), reused 0 (delta 0)
To git@github.com:rnelson0/rnelson0-custom_facts.git
 * [new branch]      master -> master
Branch master set up to track remote branch master from origin.

Before you commit your changes, let’s update the Puppetfile to reference our three new modules (I’m going to start dropping output that isn’t vital from here on out):

[rnelson0@puppet rnelson0-profile]$ cd ../puppet-tutorial/
[rnelson0@puppet puppet-tutorial]$ vi Puppetfile
...
mod "custom_facts",
  :git => "git://github.com/rnelson0/rnelson0-custom_facts"

mod "role",
  :git => "git://github.com/rnelson0/rnelson0-role"

mod "profile",
  :git => "git://github.com/rnelson0/rnelson0-profile"
[rnelson0@puppet puppet-tutorial]$ git commit -a -m 'Add custom_facts, role, and profile modules'
[rnelson0@puppet puppet-tutorial]$ git push origin

Commit and push the changes up for all three repos. Don’t forget to set up your webhooks in every repo, especially the post-receive hook on the Github side!

Assigning Roles

Now that we have a role, how do we assign it? We can easily modify the site manifest and create a node definition for server01 with the correct role. That’s fine with one server, but what if we have multiple servers in that role? Imagine for the moment that all ten VMs, server01 through server10, will be web servers. If we strip off the numbers from the hostname, we are left with server. We can define a role that all server## VMs use, creating a farm of that application type. You’ve probably seen this before with www1, www2, etc., we’re just using a different string.

Let’s flesh out our custom fact to create a ‘role’ fact based on the hostname. In our example, the pattern is very simple, /([a-z]+)[0-9]+/, and we can discard the numbers. If there are no trailing numbers, we accept the hostname as it is. In a worst case scenario, we use ‘default’. There are some complex examples out there, for instance by using the entirety of an FQDN to codify a role, environment, and instance number in the short name and a location as the DNS suffix. Instead, we’ll use the hostname fact, a shorter regex, and only create a single fact. Here’s some ruby code that creates a fact called puppet_role:

# ([a-z]+)[0-9]+, i.e. www01 or logger22 have a puppet_role of www or logger
if Facter.value(:hostname) =~ /^([a-z]+)[0-9]+$/
  Facter.add('puppet_role') do
    setcode do
      $1
    end
  end

# ([a-z]+), i.e. www or logger have a puppet_role of www or logger
elsif Facter.value(:hostname) =~ /^([a-z]+)$/
  Facter.add('puppet_role') do
    setcode do
      $1
    end
  end

# Set to hostname if no patterns match
else
  Facter.add('puppet_role') do
    setcode do
      'default'
    end
  end
end

We have one other change to make. Our site manifest only has a node definition for the master node. Let’s create an empty definition for nodes that don’t have a specified block. Add this to the bottom of your site.pp:

node default {
}

Now, commit this change and redeploy your environments. Earlier, we enabled pluginsync. The master has to synchronize as well before any clients can synchronize with it, so a simple puppet agent –test should allow the master to get the fact. You can test it afterward with facter:

[rnelson0@puppet rnelson0-custom_facts]$ sudo puppet agent --test
...
Info: Loading facts in /etc/puppet/environments/production/modules/custom_facts/lib/facter/roles.rb
...
[rnelson0@puppet rnelson0-custom_facts]$ sudo facter -p puppet_role
puppet

The next item to work on is the node server01. When you run the agent you should see the fact downloaded and the puppet_role fact populated:

[rnelson0@server01 ~]$ sudo puppet agent --test
...
Info: Loading facts in /var/lib/puppet/lib/facter/roles.rb
...
[rnelson0@server01 ~]$ sudo facter -p puppet_role
server

Putting it all together

Sweet. We’ve defined profiles, a role that uses those profiles, and a fact that can generate a puppet role for similarly named servers. How do we use these things we have created? Let’s go back to our site manifest and look at our node definitions:

[rnelson0@puppet puppet-tutorial]$ grep node manifests/site.pp
node 'puppet.nelson.va' {
node default {

We can create a node definition now for our ‘server‘ nodes and include the webserver role. It’s three simple lines, which is just the way we like it:

node /^server\d+/ {
  include role::webserver
}

Deploy that on the master. With that simple statement, an agent noop from server01 will show you that puppet is now ready to install the components of the webserver role, from the base (ssh/ntp/motd) and apache (apache) profiles.

[rnelson0@server01 ~]$ sudo puppet agent --test --noop
...
Notice: /Stage[main]/Motd/File[/etc/motd]/content:
...
Notice: Class[Ntp::Service]: Would have triggered 'refresh' from 1 events
...
Notice: /Stage[main]/Apache::Service/Service[httpd]: Would have triggered 'refresh' from 49 events
...

If everything looks good, run it again without the noop and afterward, you should be able to visit http://server01 and see an empty directory listing. If you have iptables enabled, you may need to stop it as we haven’t opened port 80 yet.

There is one last little bit to do. We went to all that work to create the fact puppet_role but did not use it. Let’s not let that effort go to waste! You can use this fact in many ways. In our example, we’ll update the Hiera hierarchy to load role-specific information. First, edit the hierarchy in /etc/hiera.yaml to look something like this:

:hierarchy:
  - defaults
  - puppet_role/%{puppet_role}
  - "%{clientcert}"
  - "%{environment}"
  - global

Hiera can examine puppet facts. In this case, it will look in the datadir (/etc/puppet/data in our setup) for a file called puppet_role/%{puppet_role}.yaml and use any data available in it. We’ll use hiera to define our classes and remove the specific node definitions from the site manifest. First, here’s the yaml:

[rnelson0@puppet rnelson0-profile]$ cat /etc/puppet/data/global.yaml
---
puppetmaster: 'puppet.nelson.va'
classes:
  profile::base
[rnelson0@puppet rnelson0-profile]$ cat /etc/puppet/data/puppet_role/server.yaml
---
classes:
  role::webserver

Second, update your site manifest by removing the server## definition and updating the default definition:

[rnelson0@puppet puppet-tutorial]$ git diff
diff --git a/manifests/site.pp b/manifests/site.pp
index 16d22b9..3757419 100644
--- a/manifests/site.pp
+++ b/manifests/site.pp
@@ -37,9 +37,6 @@ node 'puppet.nelson.va' {
   }
 }

-node /^server\d+/ {
-  include role::webserver
-}
-
 node default {
+  hiera_include('classes')
 }

The hiera_include function calls include and passes it the values found in the hiera lookup for classes. For nodes without a corresponding puppet_role or specific node definition, they will receive profile::base. Server01’s puppet_role has a corresponding hiera file, so it will receive role::webserver. Push the changes upstream, uninstall apache from server01, and re-run the agent:

[rnelson0@server01 ~]$ sudo yum remove httpd
...
[rnelson0@server01 ~]$ sudo puppet agent --test
...
Notice: /Stage[main]/Apache/Package[httpd]/ensure: created
...

With the definition now created for the puppet_role of server, you can deploy the remaining server02-server10 and all nodes will receive the role::webserver class. Just right click on your CentOS template, name your VM/set the hostname to server02 and an IP of 10.0.0.52. After the first boot, run puppet agent –test and watch the node receive the specified role.

The other node in our system is the puppet master. You can create a role::puppetmaster, based on the existing node definition and our profile::base class, and assign that to the puppet_role of puppet. I’ll leave that process as an exercise for the user.

Summary

Today, we created a custom fact and implemented the roles and profiles pattern through two modules. These three components are used together with YAML data in Hiera to allow us to deploy a single role to multiple servers of a like type. We explored simplifying the site manifest to rely on Hiera. In future sessions, we’ll expand on how to dynamically populate our hierarchy and eliminate all reliance on the site manifest.

I’d also like to thank Craig Dunn and Gary Larizza for their foundation work, via their blogs at http://www.craigdunn.org/ and http://garylarizza.com/. I’m very happy to be standing on their shoulders and I hope I’ve been able to provide some value on top of that. If you haven’t already, go read their sites, they’ve got a lot more to say about roles, profiles, and Puppet.

10 thoughts on “Intro to Roles and Profiles with Puppet and Hiera

  1. Pingback: Improved r10k deployment patterns | rnelson0
  2. Pingback: Hiera-fy your Hiera setup | rnelson0
  3. Pingback: Visible Ops Phase Three: Create A Repeatable Build Library | rnelson0
  4. Pingback: 30 Posts in 30 Days | the world needs more puppet!
  5. Pingback: Masterless Puppet and Declarative provisioning | inside.mygov.scot
  6. Pingback: What goes in a Puppet Role or Profile? | rnelson0
    • I do not see that the function is deprecated, only one of the optional parameters to it.

      Roles and profiles still looks very much like it does in this series. If you follow it further, other articles do suggest improvements, but the core remains the same – profiles that apply a single portion of technology/process and a role that composites multiple profiles together, using hiera to provide specific data as needed.

  7. Pingback: Updating Puppet classification with hiera to use the modern lookup command | rnelson0

Leave a comment