Skip to content

Instantly share code, notes, and snippets.

@KellyLSB
Last active December 9, 2016 17:38
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save KellyLSB/4315a0323ed0fe1d79b6 to your computer and use it in GitHub Desktop.
Save KellyLSB/4315a0323ed0fe1d79b6 to your computer and use it in GitHub Desktop.
Docker.IO Dynamic DNS Registration (w/ nsupdate). Non container dependent!!! Local DNS Server such as Bind is required.

Intro and Why?

So about a year ago dotcloud came out with a magical piece of software called Docker.io; a Go-lang wrapper to The Linux Container Engine (LXC). The first time I saw this I immediately jumped onto the Docker band wagon. Why, because the idea is amazing. I had one problem with it though, due to the way Docker set's up your containers. Uou either have to setup your containers using their container linking solution which puts all the container ip's into environment variables or use a service discovery tool like Skydock with Skydns. Both of whihch are Docker containers themselves and required a pre-configuration process.

The second I saw Skydock and Skydns I thought it was great and really intuitive, but I already had a network I wanted to add the containers to. For the time being I had been using the port forarding tool provided by Docker and had exposed a ton of ports on the host OS, but this made it hard to work with services running on the same port. Especially as it came to service discovery and playing with multiple Docker hosts. There had to be a better way.

I was looking at Pipework and trying to hack the virtual ethernet interfaces to be configured by DHCP, but it felt like all my efforts were in vain and not working. So then I had an idea. What if it did not matter what the IP was. What if I could let Docker do it's thing and just add the virtual network to my existing network without the need for DHCP (for Docker).

This solution listens to the Docker events stream on the host os and then pings your Bind9 DNS server with nsupdate and does a dyanmic dns update to register your service. Best part is there are NO dependnecies (aside from a network interface) for your guest os or the way that you start it.

So I created a network bridge using Ubuntu's bridge-utils package and went to work. Here is what I came up with.

How to

This guide assumes you are using Bind9 DNS with Docker on an Ubuntu 12.04+ operating system.

To start I would recomend setting up your own network bridge. You might be able to use Docker's but you probably will want to make some adjustments. I do this with the following code in my /etc/network/interfaces file.

auto docker0
iface docker0 inet static
    bridge_ports none
    bridge_fd 0
    address 10.1.0.1
    netmask 255.255.0.0
    network 10.1.0.0
    broadcast 10.1.255.255
    gateway 10.1.0.1
    dns-nameservers 10.1.0.1
    dns-search <search-domain>
    post-up route add default gw <external_interface> || /bin/true

Though to set this up there are a couple prerequistes. Just to be sure that I'm not getting any extra settings from Docker implicitly when it creates it's bridge I usually delete it and re-create it manually (ensure the Docker service is not running).

ifconfig docker0 down
brctl delbr docker0
brctl addbr docker0
ifup docker0

If you used a configuration similar to my /etc/network/interfaces line then ifup will configure your bridge for you. If you restart your machine or run service networking restart it will also handle the above automatically for you.

After you have configured your bridge with custom subnets, dns, etc... you need to update your Docker configuration to use your custom bridge (though it has the same name, I'm sourcing the variable just in case it changes how docker internally handles network configuration; never hurts to be extra careful). To do this open up your /etc/default/docker configuration file. And add/alter the DOCKER_OPTS variable to contain -b=docker0 -dns 10.1.0.1.

Once this has been completed you can take the docker_ddns file and place it in /usr/local/bin. I like to use Monit for service and process status monitoring. So here is my docker_ddns.conf file for Monit.

check process docker_ddns with pidfile /var/run/docker_ddns.pid
  start program = "/usr/local/bin/docker_ddns" with timeout 60 seconds
  stop program  = "/usr/bin/kill `cat /var/run/docker_ddns.pid`"
  if totalmem > 50.0 MB for 5 cycles then restart
  if 3 restarts within 5 cycles then timeout
  depends on docker
  group docker

I suppose you could also add the startup to your /etc/init/docker.conf though it won't ensure that the process does not die.

App Usage

docker_ddns [ /path/to/log.file | - ]

Using '-' will log output to the standard out. Defaults to '-'
#!/usr/bin/env ruby
# Docker Event Listener / DDNS
# Author: Kelly Becker <kbecker@kellybecker.me>
# Website: http://kellybecker.me
# Original Code: https://gist.github.com/KellyLSB/4315a0323ed0fe1d79b6
# License: MIT
# Set up a proper logger
require 'logger'
log_file = ARGV.first || '-'
log = Logger.new(log_file == '-' ? $stdout : log_file)
# Create a PID file for this service
File.open('/var/run/docker_ddns.pid', 'w+') { |f| f.write($$) }
# Capture the terminate signal
trap("INT") do
log.info "Caught INT Signal... Exiting."
File.unlink('/var/run/docker_ddns.pid')
sleep 1
exit
end
# Welcome message
log.info "Starting Docker Dynamic DNS - Event Handler"
log.info "Maintainer: Kelly Becker <kbecker@kellybeckr.me>"
log.info "Website: http://kellybecker.me"
# Default Configuration
ENV['DDNS_KEY'] ||= "/etc/bind/ddns.key"
ENV['NET_NS'] ||= "10.1.0.1"
ENV['NET_DOMAIN'] ||= "kellybecker.me"
ENV['DOCKER_PID'] ||= "/var/run/docker.pid"
# Ensure docker is running
time_waited = Time.now.to_i
until File.exist?(ENV['DOCKER_PID'])
if (Time.now.to_i - time_waited) > 600
log.fatal "Docker daemon still not started after 10 minutes... Please Contact Your SysAdmin!"
exit 1
end
log.warn "Docker daemon is not running yet..."
sleep 5
end
log.info "Docker Daemon UP! - Listening for Events..."
# Find CGroup Mount
File.open('/proc/mounts', 'r').each do |line|
dev, mnt, fstype, options, dump, fsck = line.split
next if "#{fstype}" != "cgroup"
next if "#{options}".include?('devices')
ENV['CGROUPMNT'] = mnt
end.close
# Exit if missing CGroup Mount
unless ENV['CGROUPMNT']
log.fatal "Could not locate cgroup mount point."
exit 1
end
# Listen To Docker.io Events
events = IO.popen('docker events')
# Keep Listening for incoming data
while line = events.gets
# Container Configuration
ENV['CONTAINER_EVENT'] = line.split.last
ENV['CONTAINER_CID_LONG'] = line.gsub(/^.*([0-9a-f]{64}).*$/i, '\1')
ENV['CONTAINER_CID'] = ENV['CONTAINER_CID_LONG'][0...12]
# Event Fired info
log.info "Event Fired (#{ENV['CONTAINER_CID']}): #{ENV['CONTAINER_EVENT']}."
case ENV['CONTAINER_EVENT']
when 'start'
# Get Container Details for DNS
ENV['CONTAINER_IP_ADDR'] = %x{docker inspect $CONTAINER_CID_LONG | grep '"IPAddress"'}.strip.gsub(/[^0-9\.]/i, '')
ENV['CONTAINER_HOSTNAME'] = %x{docker inspect $CONTAINER_CID_LONG| grep '"Hostname"' | awk '{print $NF}'}.strip.gsub(/^"(.*)",/i, '\1')
puts ENV['CONTAINER_HOSTNAME']
puts ENV['CONTAINER_IP_ADDR']
# Get Process ID of the LXC Container
ENV['NSPID'] = %x{head -n 1 $(find "$CGROUPMNT" -name $CONTAINER_CID_LONG | head -n 1)/tasks}.strip
# Ensure we have the PID
unless ENV['NSPID']
log.error "Could not find a process indentifier for container #{ENV['CONTAINER_CID']}. Cannot update DNS."
next
end
# Create the Net Namespaces
%x{mkdir -p /var/run/netns}
%x{rm -f /var/run/netns/$NSPID}
%x{ln -s /proc/$NSPID/ns/net /var/run/netns/$NSPID}
# Build the command to update the dns server
update_command = <<-UPDATE
ip netns exec $NSPID nsupdate -k $DDNS_KEY <<-EOF
server $NET_NS
zone $NET_DOMAIN.
update delete $CONTAINER_HOSTNAME.$NET_DOMAIN
update add $CONTAINER_HOSTNAME.$NET_DOMAIN 60 A $CONTAINER_IP_ADDR
send
EOF
UPDATE
# Run the nameserver update in the Net Namespace of the LXC Container
system(update_command.gsub(/^[ ]{4}/, ''))
# Message an success
if $?.success?
log.info "Updated Docker DNS (#{ENV['CONTAINER_CID']}): #{ENV['CONTAINER_HOSTNAME']}.#{ENV['NET_DOMAIN']} 60 A #{ENV['CONTAINER_IP_ADDR']}."
else
log.error "We could not update the Docker DNS records for #{ENV['CONTAINER_CID']}. Please check your nsupdate keys."
end
end
end
exit
@sdressler
Copy link

Thanks for that cool thing. However, I actually had to make some changes:

Getting the IP from the container was broken, repaired it by:

ENV['CONTAINER_IP_ADDR']  = %x{docker inspect --format '{{ .NetworkSettings.IPAddress }}' $CONTAINER_CID_LONG}

I also needed reverse updates and thus the reverse IP:

ENV['CONTAINER_IP_ADDR_REV'] = ENV['CONTAINER_IP_ADDR'].split('.').reverse.join('.').delete!("\n")

And also:

update_command = <<-UPDATE
ip netns exec $NSPID nsupdate -k $DDNS_KEY <<-EOF
server $NET_NS
update delete $CONTAINER_HOSTNAME.$NET_DOMAIN. A
update add $CONTAINER_HOSTNAME.$NET_DOMAIN. 60 A $CONTAINER_IP_ADDR

update delete $CONTAINER_IP_ADDR_REV.in-addr.arpa. PTR
update add $CONTAINER_IP_ADDR_REV.in-addr.arpa. 60 PTR $CONTAINER_HOSTNAME.$NET_DOMAIN.
send
EOF
UPDATE

(the blank line is important).

Thanks again!

@millerbill3
Copy link

Not sure if this is to do with the Docker events changing between versions or what, but I had to modify line 77 case ENV['CONTAINER_EVENT'] to case line[/start|destroy|create|attach|resize(.)\1/,0]. The original code did not retrieve the event correctly so the when clause could properly catch it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment