Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit 931d5ed7ea4e73a35c59a723dbc20b402aa0879c 0 parents
@snoremac snoremac authored
1  .gitignore
@@ -0,0 +1 @@
+.DS_Store
4 Gemfile
@@ -0,0 +1,4 @@
+source :rubygems
+
+gem "httparty"
+
14 Gemfile.lock
@@ -0,0 +1,14 @@
+GEM
+ remote: http://rubygems.org/
+ specs:
+ httparty (0.9.0)
+ multi_json (~> 1.0)
+ multi_xml
+ multi_json (1.3.6)
+ multi_xml (0.5.1)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ httparty
38 README.md
@@ -0,0 +1,38 @@
+# Summary
+
+A bunch of wrapper scripts for the Amazon EC2 API command line tools, which are intended to support a simple workflow.
+
+The basic idea is that you want to bring up one or more EC2 instances, tag each one with a FQDN and then configure Route53 DNS to point that name to the associated host.
+
+Accordingly, most of the scripts work on the assumption that each of your EC2 instances has a tag called "Name", with the value being the FQDN of the host.
+
+# System Requirements
+
+* The Amazon EC2 API Tools: http://aws.amazon.com/developertools/351
+* Java 1.6 or later (required by the EC2 API tools)
+* Ruby 1.9.3 or later with HTTParty
+
+# Environment Variables
+
+Amazon identifiers and credentials are expected to be set in the environment. Here's an example:
+
+
+ # Path to your EC2 API Tools installation
+ export EC2_HOME=~/ec2-api-tools-1.6.4
+ export PATH=$PATH:$EC2_HOME/bin
+
+ # Your AWS access Key
+ export AWS_ACCESS_KEY=AAFAYUHUIQWHHFHE
+
+ # Your AWS secret key
+ export AWS_SECRET_KEY=rdS34$dy56uf^7iHhuif56D65uDU^5yuiIjo;Mnu67
+
+ # The URL of the EC2 API endpoint in your region
+ export EC2_URL=https://ec2.us-west-1.amazonaws.com
+
+ # The zone id of the Route 53 zone you wish to contain your DNS records
+ export ROUTE53_ZONE_ID=Z23IUQGHOUIOWGH
+
+ # The name of the Route 53 zone you wish to contain your DNS records, e.g. example.com
+ export ROUTE53_ZONE_NAME=example.com
+
17 bin/ec2
@@ -0,0 +1,17 @@
+#!/bin/bash -eu
+
+SCRIPT_DIR=$(dirname $0)
+
+usage() {
+ echo "Usage: ec2 COMMAND ARGS"
+}
+
+if [ $# == 0 ]; then usage; exit 1; fi
+
+command=$1 && shift
+if [ ! -x $SCRIPT_DIR/ec2-$command ]; then
+ echo "No such ec2 command: $command"
+ exit 1
+fi
+
+$SCRIPT_DIR/ec2-$command $@
34 bin/ec2-list
@@ -0,0 +1,34 @@
+#!/bin/bash -e
+
+DESCRIBE_FILE=/tmp/ec2_describe.out
+CODE_RUNNING=16
+
+usage() {
+ echo "Usage: ec2 list [ -r ] PATTERN"
+}
+
+running=0
+
+while getopts "r" opt; do
+ case $opt in
+ r)
+ running=1
+ shift
+ ;;
+ \?)
+ usage
+ exit 3
+ ;;
+ esac
+done
+
+if [ $# == 0 ]; then usage; exit 1; fi
+PATTERN=$1
+
+DESCRIBE_CMD=ec2-describe-instances
+if [ "$running" == "1" ]; then
+ DESCRIBE_CMD="$DESCRIBE_CMD --filter instance-state-code=$CODE_RUNNING"
+fi
+
+$DESCRIBE_CMD > $DESCRIBE_FILE
+awk '/^TAG.*Name\t'"$PATTERN"'$/ { print $3, $5 }' $DESCRIBE_FILE
36 bin/ec2-name
@@ -0,0 +1,36 @@
+#!/bin/bash -e
+
+DESCRIBE_FILE=/tmp/ec2_describe.out
+
+usage() {
+ echo "Usage: ec2 name [ -d ] INSTANCE_ID FQDN"
+}
+
+create_dns=0
+
+while getopts "d" opt; do
+ case $opt in
+ d)
+ create_dns=1
+ shift
+ ;;
+ \?)
+ usage
+ exit 3
+ ;;
+ esac
+done
+
+if [ $# != 2 ]; then usage; exit 3; fi
+
+instance_id=$1
+fqdn=$2
+
+ec2-create-tags $instance_id --tag "Name=$fqdn"
+
+ec2-describe-instances $instance_id > $DESCRIBE_FILE
+ec2_hostname=$(awk '/INSTANCE/ { print $4 }' $DESCRIBE_FILE)
+
+if [ "$create_dns" == "1" ]; then
+ $(dirname $0)/route53-create -t CNAME ${fqdn}. $ec2_hostname
+fi
102 bin/ec2-run
@@ -0,0 +1,102 @@
+#!/bin/bash -e
+
+USERDATA_FILE=/tmp/ec2.userdata
+LAUNCH_FILE=/tmp/ec2_launch.out
+DESCRIBE_FILE=/tmp/ec2_describe.out
+
+usage() {
+ echo "Usage: ec2 run [ -g GROUP,... ]] AMI FQDN"
+}
+
+write_userdata() {
+cat <<-EOF > $USERDATA_FILE
+ #!/bin/bash
+
+ set -xeu
+
+ hostname $hostname
+ echo "$hostname" > /etc/hostname
+
+ awk '!/127\.0\.0\.1|127\.0\.1\.1/' /etc/hosts > /tmp/hosts
+ echo "127.0.0.1 $hostname $fqdn" > /etc/hosts
+ cat /tmp/hosts >> /etc/hosts
+EOF
+}
+
+launch_instance() {
+ ec2_command="ec2-run-instances $ami \
+ -t m1.large \
+ -b /dev/sda1=:100 \
+ -k psn-production \
+ -f $USERDATA_FILE"
+
+ groups=$(echo $groups | tr , " ")
+ for group in $groups; do
+ ec2_command="$ec2_command -g $group"
+ done
+
+ $ec2_command > $LAUNCH_FILE
+ ec2_instance_id=$(awk '/INSTANCE/ { print $2 }' $LAUNCH_FILE)
+}
+
+tag_instance() {
+ ec2-create-tags $ec2_instance_id --tag "Name=$fqdn"
+}
+
+wait_for_instance_address() {
+ set +e
+ pending=1
+
+ while [ $pending != 0 ]; do
+ ec2-describe-instances $ec2_instance_id > $DESCRIBE_FILE
+ grep running $DESCRIBE_FILE > /dev/null
+ pending=$?
+ done
+
+ set -e
+
+ ec2_ip_address=$(awk '/INSTANCE/ { print $14 }' $DESCRIBE_FILE)
+ ec2_hostname=$(awk '/INSTANCE/ { print $4 }' $DESCRIBE_FILE)
+}
+
+create_rrset() {
+ $(dirname $0)/route53-create -t CNAME ${fqdn}. $ec2_hostname
+}
+
+if [ $# == 0 ]; then usage; exit 1; fi
+
+while getopts "g:" opt; do
+ case $opt in
+ g)
+ groups=$OPTARG
+ shift 2
+ ;;
+ \?)
+ usage
+ exit 3
+ ;;
+ esac
+done
+
+if [ $# != 2 ]; then usage; exit 1; fi
+
+ami=$1
+fqdn=$2
+hostname=$(echo $fqdn | cut -f 1 -d .)
+
+write_userdata
+
+echo "Launching instance with hostname $fqdn from AMI $ami"
+
+launch_instance
+
+echo "EC2 Instance ID: $ec2_instance_id"
+
+tag_instance
+wait_for_instance_address
+
+echo "EC2 IP Address: $ec2_ip_address"
+echo "EC2 Hostname: $ec2_hostname"
+echo "PSN Hostname: $fqdn"
+
+create_rrset
64 bin/route53-create
@@ -0,0 +1,64 @@
+#!/usr/bin/env ruby
+
+$: << File.expand_path("../../lib", __FILE__)
+
+require 'rubygems'
+require 'getoptlong'
+require 'route53'
+
+ROUTE53_ZONE_ID = ENV['ROUTE53_ZONE_ID']
+ROUTE53_ZONE_NAME = ENV['ROUTE53_ZONE_NAME']
+
+def usage
+ puts "Usage: $0 [ -t TYPE ] [ -u ] NAME VALUE"
+end
+
+type = "A"
+update_only = false
+
+opts = GetoptLong.new(
+ [ "--type", "-t", GetoptLong::REQUIRED_ARGUMENT ],
+ [ "--update-only", "-u", GetoptLong::NO_ARGUMENT ]
+)
+
+begin
+ opts.each do |opt, arg|
+ case opt
+ when "--type"
+ type = arg.upcase
+ when "--update-only"
+ update_only = true
+ end
+ end
+rescue GetoptLong::Error => e
+ usage
+end
+
+
+unless ARGV.length == 2
+ usage
+ exit 1
+end
+
+name = ARGV[0]
+value = ARGV[1]
+
+puts "Checking for existing record set"
+list_response = Route53.list_rrsets ROUTE53_ZONE_ID, ROUTE53_ZONE_NAME
+
+matching_rrset = list_response.detect { |item| item[:name] == name}
+if matching_rrset
+ matching_rrset[:values].each do |existing_value|
+ puts "Deleting record #{matching_rrset[:name]} -> #{existing_value} (#{matching_rrset[:type]})"
+ Route53.delete_rrset ROUTE53_ZONE_ID, matching_rrset[:name], matching_rrset[:type], existing_value
+ end
+else
+ puts "None found"
+ if update_only
+ puts "Not created"
+ exit 0
+ end
+end
+
+puts "Creating record set #{name} -> #{value} (#{type})"
+Route53.create_rrset ROUTE53_ZONE_ID, name, type, value
90 lib/route53.rb
@@ -0,0 +1,90 @@
+require 'base64'
+require 'httparty'
+require 'openssl'
+require 'ostruct'
+require 'erb'
+require 'rexml/document'
+
+include REXML
+
+AWS_ACCESS_KEY = ENV['AWS_ACCESS_KEY']
+AWS_SECRET_KEY = ENV['AWS_SECRET_KEY']
+ROUTE53_HOST = "route53.amazonaws.com"
+RRSET_TEMPLATE = File.expand_path("../route53_rrset.erb", __FILE__)
+DEFAULT_TTL = 300
+RRSET_XPATH = "ListResourceRecordSetsResponse/ResourceRecordSets/ResourceRecordSet"
+RR_XPATH = "ResourceRecords/ResourceRecord"
+
+class ErbStruct < OpenStruct
+ def get_binding
+ binding
+ end
+end
+
+module Route53
+
+ def self.base_uri zone_id
+ "https://#{ROUTE53_HOST}/2012-02-29/hostedzone/#{zone_id}"
+ end
+
+ def self.current_date
+ response = HTTParty.get "https://#{ROUTE53_HOST}/date"
+ response.headers['date']
+ end
+
+ def self.sign text, key
+ digest = OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha256'), key, text)
+ Base64.encode64(digest)
+ end
+
+ def self.headers
+ date = current_date
+ request_auth_header = "AWS3-HTTPS AWSAccessKeyId=#{AWS_ACCESS_KEY}"
+ request_auth_header += ",Algorithm=HmacSHA256,Signature=#{sign(date, AWS_SECRET_KEY)}"
+
+ {
+ "Host" => ROUTE53_HOST,
+ "Date" => date,
+ "Content-Type" => "text/xml; charset=UTF-8",
+ "X-Amzn-Authorization" => request_auth_header
+ }
+ end
+
+ def self.list_rrsets zone_id, name
+ response = HTTParty.get("#{base_uri(zone_id)}/rrset?name=#{name}", :headers => headers)
+ response_dom = Document.new response.body
+ return response_dom.elements.collect(RRSET_XPATH) do |recordset_element|
+ {
+ :name => recordset_element.elements["Name"].text,
+ :type => recordset_element.elements["Type"].text,
+ :values => recordset_element.elements.collect(RR_XPATH) { |record_element| record_element.elements["Value"].text }
+ }
+ end
+ end
+
+ def self.create_rrset zone_id, name, type, value
+ modify_rrset "CREATE", zone_id, name, type, value
+ end
+
+ def self.delete_rrset zone_id, name, type, value
+ modify_rrset "DELETE", zone_id, name, type, value
+ end
+
+ def self.modify_rrset action, zone_id, name, type, value
+ payload_data = ErbStruct.new(
+ {:action => action, :name => name, :type => type, :ttl => DEFAULT_TTL, :value => value}
+ )
+ payload = ERB.new(File.new(RRSET_TEMPLATE).read).result(payload_data.get_binding)
+
+ response = HTTParty.post("#{base_uri(zone_id)}/rrset", :body => payload, :headers => headers)
+
+ if response.code != 200
+ response_dom = Document.new response.body
+ error_code = response_dom.root.elements["Error/Code"].text
+ error_message = response_dom.root.elements["Error/Message"].text
+ raise "Failed to create recordset: (#{error_code}) #{error_message}"
+ end
+ end
+
+
+end
20 lib/route53_rrset.erb
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2012-02-29/">
+ <ChangeBatch>
+ <Changes>
+ <Change>
+ <Action><%= action %></Action>
+ <ResourceRecordSet>
+ <Name><%= name %></Name>
+ <Type><%= type %></Type>
+ <TTL><%= ttl %></TTL>
+ <ResourceRecords>
+ <ResourceRecord>
+ <Value><%= value %></Value>
+ </ResourceRecord>
+ </ResourceRecords>
+ </ResourceRecordSet>
+ </Change>
+ </Changes>
+ </ChangeBatch>
+</ChangeResourceRecordSetsRequest>
Please sign in to comment.
Something went wrong with that request. Please try again.