How I Classify Puppet Nodes
By
- 3 minutes read - 557 wordsThe basics of defining what modules get applied to a particular node is really simple in Puppet. Out of the box you just use the hostname and the FQDN and everyone is happy. You find this everywhere in documentation, blog posts, presentations, etc. However is has a problem: scale.
What if you have an elastic infrastructure with nodes being created and destroyed automatically? What if you want to use the same manifests in different environment, but use different hostnames? What if you have stupidly complex host naming conventions that you cannot get your head round (current day job problem for me :-( )?
In all these cases and more, using the hostname to classify the node falls down. I like to add in Role that can then be access in 2 ways. With Hiera, one could do something like:
:hierarchy:
- "nodes/%{::trusted.certname}"
- "roles/%{role}"
- "%{environment}"
- "%{osfamily}-osreleasemajor"
- global
And with in site.pp
we can add in a simple case
statement:
node default {
case $::role {
'loadbalancer': {
class { 'haproxy': }
}
'db': {
class { 'mysql': }
}
default: {
notify('no specific classes assigned')
}
}
class { 'security': }
}
Now, we can still classify nodes individually but there is something in between the wider environment and OS categories that we can define ourselves. Of course we now need to define the role, which is everywhere from simple to complex or even not completely clear in my head for now.
I create a custom role fact that my manifests will look at. This is universal, no matter what mechanism is used to populate that fact that is the only place I will search in my Puppet code.
When your nodes are under Openstack or EC2, this is simple. They both have the concept of user-defined metadata as key-value pairs. I simple add a role pair:
nova meta <instance-id> set role=loadbalancer
You can also set this when you create the instance.
nova boot --meta role=loadbalancer --<other-settings> <hostname>
Now we just need the fact to look it up.
require 'net/http'
require 'json'
require 'uri'
module RoleModule
def self.add_facts
Facter.add("role") do
productname = Facter.value(:productname)
case productname
when 'OpenStack Nova'
setcode do
url= "http://169.254.169.254/openstack/latest/meta_data.json"
uri = URI.parse(url)
http = Net::HTTP.new(uri.host,uri.port)
response = http.get(uri.path)
JSON.parse(response.body)['meta']['role']
end
when 'ProLiant MicroServer'
setcode do
'lab-compute'
end
end
end
end
end
RoleModule.add_facts
What is happening here? First it checks the productname fact so it can work out what to do. If that is OpenStack Nova then it knows that is needs to look in the Openstack Metadata service (http://169.254.169.254/openstack/latest/meta_data.json). Our key/value pair is returned as part of that JSON data and is pushed in to the role fact.
Likewise, if the productname is an HP Microserver, it will always be a lab compute node (in my case).
Physical machines otherwise fall down here. There is no way to dynamically modify their role, but I have a couple of solutions:
- Part of the kickstart file for provisioning the node could populate
a configuration file (
/etc/role.conf
). If thevirtual
fact containsphysical
the role fact goes and looks it up from there. - A seperate node classification service that returns a role based on the contents of various facts that are passed via the custom fact code.
The important part with both of these is the classification is totally seperate from my Puppet code.