Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Route53 Autoconfig #8

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions configure-s3-website.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ spec = Gem::Specification.new do |s|
s.bindir = 'bin'

s.add_dependency 'deep_merge', '= 1.0.0'
s.add_dependency 'route53', '= 0.3.0'

s.add_development_dependency 'rspec', '~> 2.10.0'
s.add_development_dependency 'rspec-expectations', '~> 2.10.0'
Expand Down
1 change: 1 addition & 0 deletions lib/configure-s3-website.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'configure-s3-website/version'
require 'configure-s3-website/s3_client'
require 'configure-s3-website/cloudfront_client'
require 'configure-s3-website/route53_client'
require 'configure-s3-website/xml_helper'
require 'configure-s3-website/http_helper'
require 'configure-s3-website/runner'
Expand Down
6 changes: 6 additions & 0 deletions lib/configure-s3-website/config_source/config_source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,11 @@ def cloudfront_distribution_id

def cloudfront_distribution_id=(dist_id)
end

def redirect_domains
end

def route53_enabled
end
end
end
8 changes: 8 additions & 0 deletions lib/configure-s3-website/config_source/file_config_source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ def cloudfront_distribution_id=(dist_id)
end
end

def redirect_domains
@config['redirect_domains']
end

def route53_enabled
@config['route53']
end

private

def self.parse_config(yaml_file_path)
Expand Down
173 changes: 173 additions & 0 deletions lib/configure-s3-website/route53_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
require 'route53'

module ConfigureS3Website
class Route53Client
def initialize(options)
@config_source = options[:config_source]

# Set up the connection to route53 and store in @conn
@conn = Route53::Connection.new(@config_source.s3_access_key_id, @config_source.s3_secret_access_key)
end

def apply

# Set the domain for the site
domain = get_domain_name @config_source.s3_bucket_name

# Check to ensure that there is a hosted zone for the given domain
zone = check_and_create_hosted_zone_if_user_agrees domain
if not zone.nil?

# Create a route for the main s3_bucket
check_and_create_route(@config_source.s3_bucket_name, zone)

# Create routes for redirect urls
unless @config_source.redirect_domains.nil?
check_and_create_redirect_routes(@config_source.redirect_domains, domain, zone)
end
end
end

private

def check_and_create_redirect_routes(redirect_domains, domain, zone)
redirect_domains.each do |url|
# check to see if the domain of the redirect_urls matches the domain of the main bucket (s3_bucket_name)
redirect_domain = get_domain_name url
if redirect_domain != domain
redirect_zone = check_and_create_hosted_zone_if_user_agrees redirect_domain
unless redirect_zone.nil?
# Just create the route here, there is no need to check if it exists because we just created the
# new zone.
create_route(url, redirect_zone)
end
else
# Check to see if the route exists and create route to the specific redirect_url
check_and_create_route(url, zone)
end
end
end

def check_and_create_route(url, zone)
if route_exists?(url, zone)
# Ask the user if he/she wants to delete & recreate the route
puts "A route already exists for #{url}"
puts 'Do you want to re-create the existing entry and point it to your s3 bucket/Cloud Front?[y/N]'
case gets.chomp
when /(y|Y)/
create_route(url, zone) if remove_route(url, zone)
end
else
create_route(url, zone)
end
end

def remove_route(url, zone)
records = zone.get_records
records = records.select {|rec| rec.name.include? url}
if records.length == 1
domain_record = records[0]
domain_record.delete
true
else
puts "Unable to remove record for #{url}, please do it in the AWS Management console."
false
end
end

def create_route(url, zone)

if not @config_source.cloudfront_distribution_id.nil? and url == @config_source.s3_bucket_name
# Then this needs to point to Cloud front
# From http://docs.aws.amazon.com/Route53/latest/APIReference/API_ChangeResourceRecordSets.html#change-rrsets-request-hosted-zone-id
# and http://docs.aws.amazon.com/general/latest/gr/rande.html#cf_region
hosted_zone_id = 'Z2FDTNDATAQYW2'
redirect_url = 'cloudfront.amazonaws.com.'
else
# Get the location of the s3-bucket (need a URL for the domain redirect)
redirect_url, hosted_zone_id = S3Client.get_endpoint(@config_source, @config_source.s3_bucket_name)
end
new_record = Route53::DNSRecord.new(url, 'A', '', [redirect_url], zone, hosted_zone_id)
resp = new_record.create
if resp.error?
puts resp
else
puts "Route53 Entry created for: #{url} pointing to #{redirect_url}"
end
end

def get_domain_name(bucket)
bucket_name = bucket
parts = bucket_name.split('.')
domain = "#{parts.last(2).join('.')}."
end

def hosted_zone_exits?(domain)
# Check to see if the user has already created a hosted zone

zone = get_zone domain

not zone.nil?
end

def get_zone(domain)
# Get an array of the user's zones (usually one per domain)
zones = @conn.get_zones

# Try and find the zone for the domain of the current blog
zone = zones.select { |zone| zone.name == domain}[0]
end

def check_and_create_hosted_zone_if_user_agrees(domain)
zone_exists = ask_user_to_create_zone domain
if zone_exists
zone = get_zone domain
else
puts "Please create a hosted zone for #{domain} at: \n" +
"https://console.aws.amazon.com/route53/home before \n" +
"trying to auto-configure route53."
zone = nil
end
return zone
end

def ask_user_to_create_zone(domain)
if not hosted_zone_exits?(domain) # We need to have the user create the zone first.
puts "A hosted zone for #{domain} does not exist, create one now?[y/N]?"
case gets.chomp
when /(y|Y)/
# Create a new zone object
zone = Route53::Zone.new(domain, nil, @conn)
# Send the request to Amazon
resp = zone.create_zone
if resp.error? # The response failed, show the user the error
puts resp
zone_exists = false
else
while resp.pending?
sleep 1 # Wait for the response to finish so that we can create routes on the zone
end
zone_exists = true
end
else # The user doesn't want to create a zone at this time
zone_exists = false
end
else # the domain already exists
zone_exists = true
end
zone_exists
end

def route_exists?(url, zone)
# checks to see if the route already is in DNS (maybe it has been set to something other than s3)
records = zone.get_records
record = records.find {|rec| rec.name.include? url}
if record.nil?
return false
else
return true
end
end

end
end
9 changes: 9 additions & 0 deletions lib/configure-s3-website/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class Runner
def self.run(options, standard_input = STDIN)
S3Client.configure_website options
maybe_create_or_update_cloudfront options, standard_input
maybe_create_or_update_route53 options, standard_input
end

private
Expand All @@ -18,6 +19,14 @@ def self.maybe_create_or_update_cloudfront(options, standard_input)
end
end

def self.maybe_create_or_update_route53(options, standard_input)
route53_enabled = options[:config_source].route53_enabled
unless route53_enabled.nil? or not route53_enabled
route53_client = Route53Client.new(options)
route53_client.apply
end
end

def self.user_already_has_cf_configured(options)
config_source = options[:config_source]
config_source.cloudfront_distribution_id
Expand Down
85 changes: 70 additions & 15 deletions lib/configure-s3-website/s3_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def self.configure_website(options)
enable_website_configuration(config_source)
make_bucket_readable_to_everyone(config_source)
configure_bucket_redirects(config_source)
configure_sub_domain_redirects(config_source)
rescue NoSuchBucketError
create_bucket(config_source)
retry
Expand Down Expand Up @@ -40,6 +41,55 @@ def self.enable_website_configuration(config_source)
puts "Bucket #{config_source.s3_bucket_name} now functions as a website"
end

def self.configure_sub_domain_redirects(config_source)
# Create buckets for each sub domain
unless config_source.redirect_domains.nil?
config_source.redirect_domains.each do |domain|
begin
enable_website_domain_redirects(config_source, domain)
rescue NoSuchBucketError
create_bucket(config_source, domain)
retry
end
end
end
end

def self.enable_website_domain_redirects(config_source, bucket)
body = %|
<WebsiteConfiguration xmlns='http://s3.amazonaws.com/doc/2006-03-01/'>
<RedirectAllRequestsTo>
<HostName>#{config_source.s3_bucket_name}</HostName>
</RedirectAllRequestsTo>
</WebsiteConfiguration>
|
HttpHelper.call_s3_api(
path = "/#{bucket}/?website",
method = Net::HTTP::Put,
body = body,
config_source = config_source
)
puts "Bucket #{bucket} now redirects to #{config_source.s3_bucket_name}"
end

def self.get_endpoint(config_source, bucket)
# Need a reliable way to get the end point of existing buckets so that I
# can do proper redirects in Route53Client

# NOTES: I was going to send a request and get back the endpoint for the bucket,
# but that can't be done with SOAP. May want to look at moving to a REST API.
#
# That is why I ended up just reading the endpoint from the config file as done
# in the create function below. In the future, we should query to find the endpoint
# for a specific bucket.
endpoint = Endpoint.new(config_source.s3_endpoint || '')

# return the website endpoint of the location & the hosted_zone_id
website_endpoint = endpoint.location_constraints[endpoint.location_constraint][:website_endpoint]
hosted_zone_id = endpoint.location_constraints[endpoint.location_constraint][:hosted_zone_id]
return website_endpoint, hosted_zone_id
end

def self.make_bucket_readable_to_everyone(config_source)
policy_json = %|{
"Version":"2008-10-17",
Expand Down Expand Up @@ -99,7 +149,11 @@ def self.configure_bucket_redirects(config_source)
end
end

def self.create_bucket(config_source)
def self.create_bucket(config_source, alt_bucket_name=nil)
bucket_name = config_source.s3_bucket_name
unless alt_bucket_name.nil?
bucket_name = alt_bucket_name
end
endpoint = Endpoint.new(config_source.s3_endpoint || '')
body = if endpoint.region == 'US Standard'
'' # The standard endpoint does not need a location constraint
Expand All @@ -112,14 +166,14 @@ def self.create_bucket(config_source)
end

HttpHelper.call_s3_api(
path = "/#{config_source.s3_bucket_name}",
path = "/#{bucket_name}",
method = Net::HTTP::Put,
body = body,
config_source = config_source
)
puts "Created bucket %s in the %s Region" %
[
config_source.s3_bucket_name,
bucket_name,
endpoint.region
]
end
Expand All @@ -133,32 +187,33 @@ class Endpoint
attr_reader :region, :location_constraint, :hostname, :website_hostname

def initialize(location_constraint)
raise InvalidS3LocationConstraintError unless
location_constraints.has_key?location_constraint
raise InvalidS3LocationConstraintError unless location_constraints.has_key? location_constraint
@region = location_constraints.fetch(location_constraint)[:region]
@hostname = location_constraints.fetch(location_constraint)[:endpoint]
@website_hostname = location_constraints.fetch(location_constraint)[:website_endpoint]
@location_constraint = location_constraint
end

# http://docs.amazonwebservices.com/general/latest/gr/rande.html#s3_region
# Added hosted zone info too (needed for route53 redirects)
def location_constraints
eu_west_1_region = {
:region => 'EU (Ireland)',
:website_hostname => 's3-website-eu-west-1.amazonaws.com',
:endpoint => 's3-eu-west-1.amazonaws.com'
:endpoint => 's3-eu-west-1.amazonaws.com',
:hosted_zone_id => 'Z1BKCTXD74EZP#'
}

{
'' => { :region => 'US Standard', :endpoint => 's3.amazonaws.com', :website_endpoint => 's3-website-us-east-1.amazonaws.com' },
'us-west-2' => { :region => 'US West (Oregon)', :endpoint => 's3-us-west-2.amazonaws.com', :website_endpoint => 's3-website-us-west-2.amazonaws.com' },
'us-west-1' => { :region => 'US West (Northern California)', :endpoint => 's3-us-west-1.amazonaws.com', :website_endpoint => 's3-website-us-west-1.amazonaws.com' },
'EU' => eu_west_1_region,
'eu-west-1' => eu_west_1_region,
'ap-southeast-1' => { :region => 'Asia Pacific (Singapore)', :endpoint => 's3-ap-southeast-1.amazonaws.com', :website_endpoint => 's3-website-ap-southeast-1.amazonaws.com' },
'ap-southeast-2' => { :region => 'Asia Pacific (Sydney)', :endpoint => 's3-ap-southeast-2.amazonaws.com', :website_endpoint => 's3-website-ap-southeast-2.amazonaws.com' },
'ap-northeast-1' => { :region => 'Asia Pacific (Tokyo)', :endpoint => 's3-ap-northeast-1.amazonaws.com', :website_endpoint => 's3-website-ap-northeast-1.amazonaws.com' },
'sa-east-1' => { :region => 'South America (Sao Paulo)', :endpoint => 's3-sa-east-1.amazonaws.com', :website_endpoint => 's3-website-sa-east-1.amazonaws.com' }
'' => {:region => 'US Standard', :endpoint => 's3.amazonaws.com', :website_endpoint => 's3-website-us-east-1.amazonaws.com', :hosted_zone_id => 'Z3AQBSTGFYJSTF'},
'us-west-2' => {:region => 'US West (Oregon)', :endpoint => 's3-us-west-2.amazonaws.com', :website_endpoint => 's3-website-us-west-2.amazonaws.com', :hosted_zone_id => 'Z3BJ6K6RIION7M'},
'us-west-1' => {:region => 'US West (Northern California)', :endpoint => 's3-us-west-1.amazonaws.com', :website_endpoint => 's3-website-us-west-1.amazonaws.com', :hosted_zone_id => 'Z2F56UZL2M1ACD'},
'EU' => eu_west_1_region,
'eu-west-1' => eu_west_1_region,
'ap-southeast-1' => {:region => 'Asia Pacific (Singapore)', :endpoint => 's3-ap-southeast-1.amazonaws.com', :website_endpoint => 's3-website-ap-southeast-1.amazonaws.com', :hosted_zone_id => 'Z3O0J2DXBE1FTB'},
'ap-southeast-2' => {:region => 'Asia Pacific (Sydney)', :endpoint => 's3-ap-southeast-2.amazonaws.com', :website_endpoint => 's3-website-ap-southeast-2.amazonaws.com', :hosted_zone_id => 'Z1WCIGYICN2BYD'},
'ap-northeast-1' => {:region => 'Asia Pacific (Tokyo)', :endpoint => 's3-ap-northeast-1.amazonaws.com', :website_endpoint => 's3-website-ap-northeast-1.amazonaws.com', :hosted_zone_id => 'Z2M4EHUR26P7ZW'},
'sa-east-1' => {:region => 'South America (Sao Paulo)', :endpoint => 's3-sa-east-1.amazonaws.com', :website_endpoint => 's3-website-sa-east-1.amazonaws.com', :hosted_zone_id => 'Z7KQH4QJS55SO'}
}
end

Expand Down