Exporting resources to other agents

Puppet is commonly used to configure whole clusters of servers or HPC workers. Any configuration management system makes this task very efficient in comparison to manual care. Manifests can be shared between similar nodes. Configuration items that require individual customization per node are modeled individually. The whole process is very natural and direct.

On the other hand, there are certain configuration tasks that do not lend themselves well to the paradigm of the central definition of all state. For example, a cluster setup might include the sharing of a generated key or registering IP addresses of peer nodes as they become available. An automatic setup should include an exchange of such shared information. Puppet can help out with this as well.

This is a very good fit. It saves a metalayer, because you don't need to implement the setup of an information exchange system in Puppet. The sharing is secure, relying on Puppet's authentication and encryption infrastructure. There is logging and central control over the deployment of the shared configuration. Puppet retains its role as the central source for all system details; it serves as a hub for a secure exchange of information.

Exporting and importing resources

Puppet approaches the problem of sharing configuration information among multiple agent nodes by way of exported resources. The concept is simple. The manifest of node A can contain one or more resources that are purely virtual and not for realization in the manifest of this node A. Other nodes, such as B and C, can import some or all of these resources. Then, the resources become part of the catalogs of these remote nodes.

Exporting and importing resources

The syntax to import and export resources is very similar to that of virtual resources. An exported resource is declared by prepending the resource type name with two @ characters:

@@file { 'my-app-psk': 
  ensure  => file,
  path    => '/etc/my-app/psk', 
  content => 'nwNFgzsn9n3sDfnFANfoinaAEF', 
  tag     => 'cluster02', 
}

The importing manifests collect these resources using an expression, which is again similar to the collection of virtual resources but with double-angled brackets, < and >:

File<<| tag == 'cluster02' |>>

Tags are a very common way to take fine-grained control over the distribution of such exported resources.

Configuring the master to store exported resources

The only recommendable way to enable support for exported resources is PuppetDB. It is a domain-specific database implementation that stores different kinds of data that the Puppet master deals with during regular operation. This includes catalog requests from agents (including their valuable facts), reports from catalog applications, and exported resources.

Chapter 2, The Master and Its Agents, detailed a manual installation of the master. Let's add the PuppetDB with more style—through Puppet! On the Forge, you will find a convenient module that will make this easy:

puppet module install puppetlabs-puppetdb

On the master node, the setup now becomes a one-line invocation:

puppet apply -e 'include puppetdb, puppetdb::master::config'

As our test master uses a nonstandard SSL certificate that is named master.example.net (instead of its FQDN), it must be configured for puppetdb as well:

include puppetdb
class { 
  'puppetdb::master::config': 
    puppetdb_server => 'master.example.net' 
}

The ensuing catalog run is quite impressive. Puppet installs the PostgreSQL backend, the Jetty server, and the actual PuppetDB package, and it configures everything and starts the services up—all in one go. After applying this short manifest, you have added a complex piece of infrastructure to your Puppet setup. You can now use exported resources for a variety of helpful tasks.

Exporting SSH host keys

For homegrown interactions between clustered machines, SSH can be an invaluable tool. File transfer and remote execution of arbitrary commands is easily possible thanks to the ubiquitous sshd service. For security reasons, each host generates a unique key in order to identify itself. Of course, such public key authentication systems can only really work with a trust network, or the presharing of the public keys.

Puppet can do the latter quite nicely:

@@sshkey { $::fqdn: 
  host_aliases => $::hostname, 
  key          => $::sshecdsakey, 
  tag          => 'san-nyc' 
} 

Interested nodes collect keys with the known pattern:

Sshkey<<| tag == 'san-nyc' |>>

Now, SSH servers can be authenticated through the respective keys that Puppet safely stores in its database. As always, the Puppet master is the fulcrum of security.

Tip

As a matter of fact, some ssh modules from the Puppet Forge will use this kind of construct to do this work for you.

Managing hosts files locally

Many sites can rely on a local DNS infrastructure. Resolving names to local IP addresses is easy with such setups. However, small networks or sites that consist of many independent clusters with little shared infrastructure will have to rely on names in /etc/hosts instead.

You can maintain a central hosts file per network cell, or you can make Puppet maintain each entry in each hosts file separately. The latter approach has some advantages:

  • Changes are automatically distributed through the Puppet agent network
  • Puppet copes with unmanaged lines in the hosts files

A manually maintained registry is prone to become outdated every once in a while. It will also obliterate local additions in any hosts files on the agent machines.

The manifest implementation of the superior approach with exported resources is very similar to the sshkey example from the previous section:

@@host { $::fqdn: 
  ip           => $::ipaddress,
  host_aliases => [ $::hostname ],
  tag          => 'nyc-site',
}

This is the same principle, only now, each node exports its $ipaddress fact value alongside its name and not a public key. The import also works the same way:

Host<<| tag == 'nyc-site' |>>

Automating custom configuration items

Do you remember the Cacti module that you created during the previous chapter? It makes it very simple to configure all monitored devices in the manifest of the Cacti server. However, as this is possible, wouldn't it be even better if each node in your network was registered automatically with Cacti? It's simple: make the devices export their respective cacti_device resources for the server to collect:

@@cacti_device { $::fqdn: 
  ensure => present, 
  ip     => $::ipaddress, 
  tag    => 'nyc-site', 
} 

The Cacti server, apart from including the cacti class, just needs to collect the devices now:

Cacti_device<<| tag == 'nyc-site' |>>

If one Cacti server handles all your machines, you can just omit the tag comparison:

Cacti_device<<| |>>

Once the module supports other Cacti resources, you can handle them in the same way. Let's look at an example from another popular monitoring solution.

Simplifying the configuration of Nagios

Puppet comes with support to manage the complete configuration of Nagios (and compatible versions of Icinga). Each configuration section can be represented by a distinct Puppet resource with types such as nagios_host or nagios_service.

Tip

There is an endeavor to remove this support from core Puppet. This does not mean that support will be discontinued, however. It will just move to yet another excellent Puppet module.

Each of your machines can export their individual nagios_host resources alongside their host and cacti_device resources. However, thanks to the diverse Nagios support, you can do even better.

Assuming that you have a module or class to wrap SSH handling (you are using a Forge module for the actual management, of course), you can handle monitoring from inside your own SSH server class. By adding the export to this class, you make sure that nodes that include the class (and only these nodes) will also get monitoring:

class site::ssh { 
  # ...actual SSH management... 
  @@nagios_service { "${::fqdn}-ssh": 
    use       => 'ssh_template', 
    host_name => $::fqdn, 
  } 
} 

You probably know the drill by now, but let's repeat the mantra once more:

Nagios_service<<| |>>

With this collection, the Nagios host configures itself with all services that the agent manifests create.

Tip

For large Nagios configurations, you might want to consider reimplementing the Nagios types yourself, using simple defines that build the configuration from templates. The native types can be slower than the file resources in this case, because they have to parse the whole Nagios configuration on each run. The file resources can be much cheaper, as they rely on content-agnostic checksums.

Maintaining your central firewall

Speaking of useful features that are not part of the core of Puppet, you can manage the rules of your iptables firewall, of course. You need the puppetlabs-firewall module to make the appropriate types available. Then, each machine can (among other useful things) export its own required port forwarding to the firewall machines:

@@firewall { "150 forward port 443 to ${::hostname}": 
  proto       => 'tcp', 
  dport       => '443', 
  destination => $public_ip_address, 
  jump        => 'DNAT', 
  todest      => $::ipaddress, 
  tag         => 'segment03', 
}

Note

The $public_ip_address value is not a Facter fact, of course. Your node will have to be configured with the appropriate information. You can refer to the next chapter for a good way to do this.

The title of a firewall rule resource conventionally begins with a three-digit index for ordering purposes. The firewall machines collect all these rules naturally:

Firewall<<| tag == 'segment03' |>>

As you can see, the possibilities for modeling distributed systems through exported Puppet resources are manifold. The simple pattern that we've iterated for several resource types suffices for a wide range of use cases. Combined with defined resource types, it allows you to flexibly enable your manifests to work together in order to form complex cluster setups with relatively little effort. The larger your clusters, the more work Puppet lifts from you through exports and collections.

Removing obsolete exports

When a node manifest stops exporting any resource, that resource's record is removed from PuppetDB once the node's catalog is compiled. This usually happens when the agent checks in with the master.

However, if you take an agent out of commission permanently, this will never happen. That's why you will need to remove those exports from the DB manually. Otherwise, other nodes will keep importing the old resources.

To clean such records from PuppetDB, use the puppet node deactivate command on the master server:

puppet node deactivate vax793.example.net