Last active
August 29, 2015 14:06
-
-
Save IronSavior/2f64534b8df3d30830a2 to your computer and use it in GitHub Desktop.
Poor man's domain transfer to Route 53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 <[email protected]> | |
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