A method for using fact based nodes with Puppet

Puppets method of assigning configuration to nodes based on their hostname works well enough in static server environments where you can guarantee that your IPs and hostnames will always be the same, however in dynamic, cloud based environments such as AWS this method can become a hinderence. Specifically assigning a role to a node and using this to draw config from the master works well in such environments.

Bootstrapping Puppet

The assumption here is that you will be bringing up instances and bootstrapping them in order to configure them so they are ready for use immediately, this could be via Cloudformation, dropping a bash script onto the instance or via some other method. During the bootstrap process you can push your puppet config onto the instance, you might have a tarballed version of your puppet config that is pushed up and unpacked onto an instance or you might check it out from Git.

Once you have your Puppet config on the instance you can perform a masterless Puppet run by specifying a manifest to run like so:

puppet apply /etc/puppet/manifests/site.pp

This will kick off a puppet run, of course this will only pick up settings for your default node since there will not be an entry for your new instances hostname which will most likely be dynamic.

So how do we specify what config to apply to the node? We can use a combination of Hiera and Facter to achieve this goal.

Specifying a role for an instance with facts

It is possible to push external, custom facts into Puppet, these facts can be used with Hiera to push a custom variable into a module in Puppet, this variable can be used to figure out what role the instance should adopt.

You can push external facts into Puppet by using files containing fact names and values, Puppet will look for external fact files in '/etc/facter/facts.d' (this may vary depending on your OS and version of Puppet).

Here's a script snippet that will ensure the fact directory exists and then create a fact file with an instance role:

mkdir -p /etc/facter
mkdir -p /etc/facter/facts.d
echo "instance_role=webserver" > /etc/facter/facts.d/instance_role.txt

You can test that the fact is working like so:

facter --debug | grep instance_role

Putting it all together with Hiera and a Puppet module

Using Hiera we can take our external fact and push it into a Puppet module as a variable, to go with this you will need a module that reads this fact based variable and applies the appropriate class to the instance.

Here's a simple hiera.yaml file that will achieve this:

:hierarchy:
    - "%{::instance_role}"
:backends:
    - yaml
:yaml:
    :datadir: '/etc/puppet/hieradata'

Of course you will need to create the hieradata dir in your Puppet config, inside this you will need to create a yaml config file for each instance role:

# file /etc/puppet/hieradata/webserver.yaml
---
instance_roles::role: 'webserver'

So as you can probably see from the above you are going to need an instance_roles module that will actually do something with this piece of information, what this piece of config is saying is "pass a variable called role with a value of webserver into a module called instance_roles".

So you will need to create the module and give it an init.pp script like this:

# file /etc/puppet/modules/instance_roles/manifests/init.pp
class instance_roles($role='') {

	notify{"instance role: '${role}'": }

	# Include any common config
	include instance_roles::roles::common

	# Include role specific config
	include "instance_roles::roles::${role}"

}

In the above class you can see that two role classes are loaded, the first one (common) can be used to include any shared config such as users, ssh keys and things like that. The second role class is loaded dynamically using the role variable set by Hiera which is driven by the custom fact set earlier.

These role classes will need to be created within the instance_role module under manifests/roles like so:

# file /etc/puppet/modules/instance_roles/manifests/roles/common.pp
class instance_roles::roles::common {
	# Common config

	include users

	include ssh

	# etc
}

# file /etc/puppet/modules/instance_roles/manifests/roles/webserver.pp
class instance_roles::roles::webserver {
	# Role specific config
	include apache

	include mysql_client

	# etc
}

You can create more instance role classes as needed to cover every type of instance that you need.

One last step to make the whole thing work is a bootstrap script that will create the fact file and apply the Puppet manifest, here's an example one:

mkdir -p /etc/facter
mkdir -p /etc/facter/facts.d
echo "instance_role=webserver" > /etc/facter/facts.d/instance_role.txt
puppet apply /etc/puppet/manifests/site.pp --modulepath=/etc/puppet/modules --fileserverconfig=/etc/puppet/fileserver.conf --hiera_config=/etc/puppet/hiera.yaml

There you have it, fact driven nodes in Puppet in a few easy steps, I've got an example of an instance role node module on Github, feel free to fork it and use it yourself.