Exposing name servers with Puppet Facts

Carrying on from the last post, I needed a good reliable way to point my Nginx configuration at a DNS server to use for resolving backends. The issue is that I wanted my Puppet module to be portable across various environments, some which block outbound DNS traffic to external services and others where the networks may be redefined on a frequent basis and maintaining an accurate list of all the name servers would be difficult (eg the cloud).

I could have used dnsmasq to setup a localhost resolver, but when it comes to operational servers, simplicity is key – having yet another daemon that could crash or cause problems is never desirable if there’s a simpler way to solve the issue.

Instead I used Facter (sic), Puppet’s tool for exposing values pulled from the system into variables that can be used in your Puppet manifests or templates. The following custom fact is included in my Puppet module and is run before any configuration is applied to the host running my Nginx configuration:

#!/usr/bin/env ruby
#
# Returns a string with all the IPs of all configured nameservers on
# the server. Useful for including into applications such as Nginx.
#
# I live in mymodulenamehere/lib/facter/nameserver_list.rb
# 

Facter.add("nameserver_list") do
    setcode do
      nameserver = false

      # Find all the nameserver values in /etc/resolv.conf
      File.open("/etc/resolv.conf", "r").each_line do |line|
        if line =~ /^nameserver\s*(\S*)/
          if nameserver
            nameserver = nameserver + " " + $1
          else
            nameserver = $1
          end
        end
      end

      # If we can't get any result (bad host config?) default to a
      # public DNS server that is likely to be reachable.
      unless nameserver
        nameserver = '8.8.8.8'
      end

      nameserver
    end
end

On a system with a typically configured /etc/resolv.conf file such as:

search example.com
nameserver 192.168.0.1
nameserver 10.1.1.1

The fact will expose the nameservers in a space-delineated string such as:

# facter -p | grep 'nameserver_list'
nameserver_list => 192.168.0.1 10.1.1.1

I can then use the Fact inside my Puppet templates for Nginx to configure the resolver:

server {
    ...
    resolver <%= @nameserver_list %>;
    resolver_timeout 1s;
    ...
}

This works pretty well, but there are a couple things to watch out for:

  1. If the Fact fails to execute at all, your configuration will be broken. Having said that, it’s a very simple Fact and there’s not a lot that really could fail (eg no dependencies on other apps/non-standard resources).
  2. Linux hosts resolve DNS using the nameservers specified in the order in /etc/resolv.conf. If one fails, they move on and try the next. However Nginx differs, and just uses the list of provides nameservers in round-robin fashion. This is fine if your nameservers are all equals, but if some are more latent or less reliable than others, it could cause slight delays.
  3. You want to drop the resolver_timeout to 1 second, to ensure a failing nameserver doesn’t hold up re-resolution of DNS for too long. Remember that this re-resolution should only occur when the TTL of the DNS records for the backend has expired, so even if one DNS server is bad, it should have almost no impact to performance for your requests.
  4. Nginx isn’t going to pickup stuff in /etc/hosts using these resolvers. This should be common sense, but thought I better put that out there just-in-case.
  5. This Ruby could be better, but I’m not a dev and hacked it up in 15mins. The regex should probably also be improved to handle some of the more exotic /etc/resolv.confs that I’m sure people manage to write.

6 thoughts on “Exposing name servers with Puppet Facts

  1. Gaurav Khanna

    Hi,

    Q. Why is the resolver_timeout to be 1s? Are the nameservers that flaky?

    Regards,
    Gaurav

    Reply
  2. Gaurav Khanna

    Guess to refine my question, is the resolver_timeout of 1s only comes into play when the name server in the order does not respond in 1s? If that is the case then it would make sense.

    Reply
  3. Gaurav Khanna

    If this is correct, then please delete my earlier query ’cause I had misunderstood the resolver_timeout initially.

    Thanks.

    Reply
  4. Ryan McKern

    Hi,
    You might want to know about the Resolv class in Ruby’s stdlib. It’ll parse resolv.conf and give you the appropriate details without having to handle the file yourself.

    >> require ‘resolv’
    => true
    >> Resolv::DNS::Config.default_config_hash
    => {
    :nameserver => [
    [0] “123.231.1.1”,
    [1] “123.231.1.2”
    ],
    :search => [
    [0] “a.thing.net”
    ],
    :ndots => 1
    }

    Reply
  5. Anonymous User

    I had the same thing issue with getting nameserver facts from /etc/resolv.conf. This post helped me get started with my first facter fact.

    require ‘resolv’

    Resolv::DNS::Config.default_config_hash.each do | key, value |
    if !value.nil?
    Facter.add(“dns_#{key}”) do
    if value.is_a?(Array)
    setcode { value.join(‘,’) }
    else
    setcode { value }
    end
    end
    end
    end

    Reply

Leave a Reply