Skip to content

Instantly share code, notes, and snippets.

@IronSavior
Last active August 29, 2015 14:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save IronSavior/2f64534b8df3d30830a2 to your computer and use it in GitHub Desktop.
Save IronSavior/2f64534b8df3d30830a2 to your computer and use it in GitHub Desktop.
Poor man's domain transfer to Route 53
#!/bin/env ruby
### The Poor-Man's DNS Zone Transfer
# Copy specific records from a specific name service to Route 53, without using a conventional zone transfer mechanism.
#
# I realize this is unorthodox and might seem less-than-optimal for most use-cases. In my case, this is a means to
# help me continue to utilize Dreamhost's "Fully Hosted Domain" features even after migrating the authority of my zones
# as well as my name service to Route 53. The correct functioning of my web hosting and my email services depend on
# the ability of dreamhost to freely manage a known subset of the records in my zone. I chose to solve this by
# periodically copying that set of records from Dreamhost's name server for my domain into its actual authoritative
# zone hosted in Route 53.
#
# It is required to specify the set of records to be copied, the source name servers, and the zone name. Each record
# set is uniquely specified by a name and type (also a class, but it's assumed to be "IN"--I don't think Route 53
# currently supports records of any other class, anyway). More than one source name server can be given, but the only
# benefit for doing so is to retry failed queries against alternate servers.
#
# WARNING:
# I published this merely as an example for whatever value it may or may not hold for anyone else. This program
# probably doesn't do what you want it to do. Use of this software is strictly at your own risk!
# * It probably won't function correctly for any case other than my own (and perhaps not even in that case).
# * It easily has the potential to cause harm. Non-exhaustive list of reasons to be cautious:
# - You give it power to create and destroy records in your Route 53 zone. Contemplate this on the Tree of Woe.
# - It patches a bug in required gem "net-dns" and can therefore affect the other software that uses this gem.
# - My case only required copying records of 3 record types: A, MX, and TXT.
# - It was not designed to handle cases where the complete set of records must be gathered from multiple name
# servers and is likely to be unsuitable for such cases.
#
# TL;DR: I give no guarantee or warranty of any kind. If your use of it breaks something, then that is your fault!
#
# License: Public Domain
# Author: Erik Elmore <erik@erikelmore.com>
require 'net/dns'
require 'aws/route_53'
zone_name = 'ironsavior.net.'
ttl = 7200
src_ns = (1..3).map{|i| Resolver("ns#{i}.dreamhost.com").answer.map(&:address) }.flatten.uniq
live_ns = Resolver(zone_name, 'NS').answer.map{ |ns|
Resolver(ns.value).answer.map(&:address).map(&:to_s)
}.flatten.uniq
# Identifiers for unique record sets in APIs
record_set_specs = Array[
[:MX],
[:A],
[:MX, 'mail'],
[:A, 'ftp'],
[:A, 'mail'],
[:A, 'mailboxes'],
[:A, 'www.mailboxes'],
[:A, 'mysql'],
[:A, 'ssh'],
[:A, 'webmail'],
[:A, 'www.webmail'],
[:A, 'www'],
[:TXT, '_domainkey'],
[:TXT, 'ironsavior.net._domainkey']
].map{ |type, dn|
Array[ [dn, zone_name].compact.join('.'), type.to_s ]
}
# Fetch credentials from the AWS CLI config file "~/.aws/config"
# You can use the AWS CLI command `aws configure` to create this file.
# This requires ENV['HOME'] to be set (which might not be true during boot)
def aws_config( opts = {} )
opts = {
profile: :default,
keys: [:aws_access_key_id, :aws_secret_access_key, :region],
file: File.expand_path('~/.aws/config')
}.merge opts
rx_for = ->(key){ /^\[#{opts[:profile]}\]\n[^\[]+\n#{key}\s*=\s*([^\s]+)$/m }
find = ->(key, body){ body.match(rx_for.(key)).captures[0] }
body = opts[:body] || File.read(opts[:file])
Hash[ opts[:keys].map{ |key|
[key.to_s.gsub('aws_', '').to_sym, find.(key, body)]
}]
end
def strict_resolver( *servers )
Net::DNS::Resolver.new(
config_file: '/dev/null',
nameservers: servers,
recurse: false,
dnsrch: false,
use_tcp: true
)
end
# For whatever reason, this method was not implemented by the net-dns gem.
class Net::DNS::RR::TXT
def value
'"%s"' % txt.strip
end
end
def query( server, name, type )
server.query(name, type).answer.map{|r| r.value.to_s }.sort
end
src_dns = strict_resolver *src_ns
live_dns = strict_resolver *live_ns
AWS.config aws_config
zone = AWS::Route53.new.hosted_zones.detect{|z| z.name == zone_name }
batch = AWS::Route53::ChangeBatch.new zone.id
AWS.memoize do
record_set_specs.each do |spec|
src_values = query src_dns, *spec
unless src_values == query(live_dns, *spec)
puts 'Updating: [%s, %s] => [%s]' % [*spec.reverse, src_values.join(', ')]
target = zone.resource_record_sets[*spec]
batch << target.new_delete_request if target.exists?
batch << AWS::Route53::CreateRequest.new(
*spec,
ttl: ttl,
resource_records: src_values.map{|v| Hash[value: v] }
)
end
end
end
batch.call unless batch.changes.empty?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment