Facter with a custom paintjob

Posted by Henry Cook on Wed 07 September 2016

A little while back I was setting up Cloudwatch Logs for some containers, and did this via the use of CloudFormation templates, Puppet and Facter.

Fortunately I was able to do all this with refs inside the CloudFormation template and echo it into a bootstrap.txt file through the EC2 instance's User Data.

The next step was to then store the CloudWatch Logs group as a custom fact to be picked up by Puppet.

Puppet can then configure the Amazon CloudWatch Logs logging driver option for the service that runs the container.


Custom Facts

What is a fact?

A fact is a key/value data pair that represents some aspect of node state, such as its IP address, uptime, operating system, or whether it's a virtual machine.

So what's a Custom Fact?

Sometimes you need to be able to write conditional expressions based on site-specific data that just isn’t available via Facter, or perhaps you’d like to include it in a template.

Since you can’t include arbitrary Ruby code in your manifests, the best solution is to add a new fact to Facter. These additional facts can then be distributed to Puppet clients and are available for use in manifests and templates, just like any other fact would be.


Example

For this situation I wanted to create a fact named cloudwatch_logs_group that contains the name of the logs groups and is stored within a bootstrap.txt that was created by the instance's User Data. Here's an example of the bootstrap.txt:

cloudwatch_logs_group=logs_group_name

Now to create the fact...

Facter searches all directories in the Ruby $LOAD_PATH variable for subdirectories named ‘facter’, and loads all Ruby files in those directories.

The first step is to create a subdir in your Puppet module called facter for the fact to be evaluated during the Puppet run.

Next we create the fact inside the facter directory, for example cloudwatch_logs_group.rb:

Facter.add('cloudwatch_logs_group') do
  bootstrap_file = "/path/to/bootstrap.txt"
  confine :some_other_fact => 'true'
  setcode do
    if bootstrap_file.exists?
      Facter::Core::Execution.execute("grep 'cloudwatch_logs_group' #{bootstrap_file} | awk -F '=' '{print $2}'")
    end
  end
end

This will successfully create the fact we're looking for however, what does it all mean?

The custom fact is created with the name cloudwatch_logs_group and then initiates the block to iterate through.

Facter.add('cloudwatch_logs_group') do

Here we're setting the variable bootstrap_file to the location of the bootstrap.txt on the host.

bootstrap_file = "/path/to/bootstrap.txt"

Confine restricts the fact to only run on systems that matches another given fact. This then means I can make sure the fact only runs on the machines I want it to run on.

confine :some_other_fact => 'true'

This is the actual meat of the code where it checks to see if the bootstrap.txt file exists. If true, it greps cloudwatch_logs_group and returns the field after the = sign. Whatever's returned by the Facter::Core::Execution.execute method is then stored as the fact's value.

In this case the value will be the CloudWatch Logs Group name.

setcode do
  if bootstrap_file.exists?
    Facter::Core::Execution.execute("grep 'cloudwatch_logs_group' #{bootstrap_file} | awk -F '=' '{print $2}'")
  end
end

Provided all goes well, you should now have a fully working Custom Fact!


Conclusion

If you're a Puppet user, I can guarantee you'll be using Facter in some way or another. Having the ability to specify a custom fact has proven (from my own personal experience) to be a powerful tool.

Custom Facts help you work around those sometimes pesky environment variables, and allows you to solve a potentially complex problem with some simple logic.