Allowing changes to a Puppet-managed resource outside of Puppet

January 29, 2020 

When a resource is managed by Puppet it is typically managed fully, or not at all. Exceptions to this rule, such as the "replace" parameter in the File resource, are rare. However, sometimes you end up having to change the properties of a resource outside of Puppet without making Puppet overwrite those changes on the next run. A good example is a set of firewall rules managed with puppetlabs-firewall - you generally want to manage the whole ruleset with Puppet so that you can be sure of the state of the ruleset. For example, if you have libvirt installed, a bunch of virbr0-related rules may appear without your consent. On RedHat-based distros you may also see several rules getting removed by puppetlabs-firewall. Or a fellow sysadmin may have added some random iptables rules for debugging purposes. These are good reasons to have

resources { 'firewall':
  purge => true,

in your manifests.

Now, what if you really need to manage a property of an iptables rule while keeping it in Puppet's control at the same time? Based on a fair bit of research the only reasonable way to do it is to create a custom fact that gets the current value of that property on the system. For example, suppose you want to direct traffic from a certain public IP range to a specific certain KVM virtual machine and be able to change the target VM at will to switch,say, between a production and staging system. You could write fact like this (which never ended up being used but which works):

# frozen_string_literal: true

Facter.add(:dnat_todest) do
  output = iptables -t nat --list-rules PREROUTING
  ip =

  output.split(/\n/).each do |line|
    match = line.match(/^-A PREROUTING .* -j DNAT --to-destination (192.168.122.\d+).*/) # rubocop:disable Metrics/LineLength
    if match
      ip = match[1]
  value = if ip.empty?
  setcode do

This code goes through iptables rules in the nat table's PREROUTING chain and does a regex match against each. When a match is found it captures the 192.168.122.x IP it found and returns that as the value of the fact. That fact can then be used in Puppet manifests as the value of a property, in this case value of the "todest" parameter in the firewall resource. If an IP cannot be determined a default value is given, under the assumption that Puppet has not yet created the rule.

On a more generic level a custom fact extracts the current value of a property on a system and that value is then used in a Puppet manifest as the desired value of the property. So, current and desired values of the property are always in sync and Puppet will thus see no need to change anything. This way Puppet can manage a resource without actually managing all of its properties. This is a like a poor-man's version of Terraform's built-in lifecycle ignore_changes feature.

Samuli Seppänen
Samuli Seppänen
Author archive