Using the multi-resource declaration and defined types to simplify manifests
Sometimes it seems you just keep repeating the same block of code with only one or two lines changed. Sometimes a single thing you need to do more than once is made up of the same two or three resources. These two scenarios are ones that I experience fairly often.
They are also ones I regularly observe when doing code reviews for others. I am often met with interest and a response that is along the lines of “I didn’t know you could do that” when I mention the idea of simplifying the code I am reviewing by using a multi-resource declaration or a defined type. This post will introduce you to multi-resource declarations and defined types and then walk you through a real-world example of putting them to use to configure load balancing of Puppet Enterprise's services.
Did you know that these two blocks of code are actually identical in function?
The version on the right is an example of a multi-resource declaration. It's not only shorter but it also centralizes the lines that are duplicated in the standard version to a
default block. This makes for easier upkeep as you have fewer places to edit common parameters like
notify. Details on the format of a multi-resource declaration can be found in the Language: Resources (advanced) docs.
To quote the docs:
Defined resource types (also called defined types or defines) are blocks of Puppet code that can be evaluated multiple times with different parameters. Once defined, they act like a new resource type: you can cause the block to be evaluated by declaring a resource of that new resource type.
One of the best examples of this is the chunk of code in the puppetlabs/apache module that allows you to create multiple Apache vhosts. As a user, you write something as simple as the example below. Everything else will be done by the defined type.
Internally the puppet manifest that defines an
apache::vhost containing a whopping 1111 lines of code! Check it out for yourself here.
This is a prime example of how using a defined type provides you with two very real benefits:
- reusable code: you are able to use the block of code as shown in the example above to create multiple unique instances of the same "thing" (a vhost in this case).
- shorter, more readable profiles: when configuring an Apache web server you are able to create the two vhosts with just a few lines of code instead of all that would be needed without the abstraction of the logic into the defined type
Exploring a real-world use case
Most of us probably are not going to write a defined type that is 1000+ lines of code but that doesn't mean we can't benefit from them anyhow. The rest of this post will walk you through a real-world example of how using defined types and multi-resource declarations drastically simplified and shortened some code in use by my team at Puppet.
The task at hand: load balancing Puppet Enterprise
I work on the InfraCore team as part of Developer Services at Puppet. I recently set out to rework the load balancers our team fronts Puppet Enterprise with internally. I had the following two goals for this work:
- route every service provided by our Puppet Enterprise installation through our HAProxy load balancers
- use Consul with HAProxy's "server-template" to automatically populate the backend server or servers for each service instead of PuppetDB
Round 1: an unreadable manifest
My first pass at this generated a profile that configured HAProxy correctly but was nearly impossible to follow. I made things a bit better by converting all the individual
haproxy::balancermember resources into multi-resource declarations. This helped make each group of resources easier to maintain while simultaneously ensuring the backend for a frontend was nowhere near it in the manifest (which is less than ideal). The manifest was laid out in this order:
- Nine different
- Three variables that each defined a non-trivial
.eachloop for each of the three variables that defined the
haproxy::balancermemberresources that corresponded to the
Here's what that looked like:
If you can easily follow what's going on in that chunk of code you are doing better than me...
Round 2: a defined type
Even though the first round "worked", I wasn't happy with it. After mulling things over a bit and looking at the code taken to create the frontend and backend I realized there was a fair amount of overlap between the two. This was also the point at which I decided to switch to using HAProxy’s server-template backed by Consul to replace PuppetDB queries so that updates to the backend servers were more real-time.
Switching to a server-template also meant that changes to the list of servers providing the backend service would not require HAProxy to restart. With that in mind, I decided to try making a defined type that took care of creating both the frontend and backend resources. I also wanted to save others from the same aggravation while I was at it so I created the ploperations/haproxy_consul module instead of hiding the code away in my control repo. The result is that I was able to replace the code above with this:
This is a prime example of how combining a defined type (
haproxy_consul::server_template) and a multi-resource declaration can make for much more readable and maintainable code. Now, let's dive into what's going on in this example...
Diving into the new defined type
The code above has two distinct things:
- a set of
- a single
haproxy::listenresource for the stats page (it doesn't get a backend defined)
Here's what's happening inside the
haproxy_consul::server_template defined type (you can also see this on GitHub here):
All I have done here is put the different resources needed for a single frontend and backend HAProxy service that is backed by Consul into a separate manifest, declared some parameters for the info needed to create those resources, and started the manifest with
define instead of
class. Back in the multi-resource declaration, I then defined a few default values that I wanted to pass to at least the majority of my services and followed that up with instances of this new type just like what was done with the
file resources back at the beginning of this post.
Going a few steps further
Since I was making a module out of this anyhow, I figured it would be good to extract a few more pieces of code from this project too. The end result was a module that also provides:
- a simplified way to make your Consul server listen on port 53 via the
- a reusable block of code for setting up connection validation between HAProxy and something using your Puppet CA via the
- a defined type named
resolverthat sets up a resolver in HAProxy that points at your Consul cluster
Seeing as this post is focusing on defined types let's take a look at the other one in the module:
This allows you to have Puppet query your Consul server for a list of nodes providing the 'consul' service and use those nodes as the resolvers defined in HAProxy. With this code you could easily set up both a test and a production resolver on the same HAProxy instance and then point some services to each one by doing something similar to this:
With the above in place you would be able to add either
resolvers prod-consul or
resolvers test-consul to the
balancermember_options of each
haproxy_consul::server_template resource similar to how the example earlier had
resolvers consul defined for each one.
Hopefully this gives you some ideas that you can use to simplify your own code. As mentioned earlier, you can find the
haproxy_consul module on the Puppet Forge and you can see its source code on GitHub.
Gene Liverman is a senior site reliability engineer at Puppet.