build-ubuntu-ami 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ (The MIT License)
2
+
3
+ Copyright © 2012 Aaron Suggs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,28 @@
1
+ # Build Ubuntu AMI
2
+
3
+ A simple, secure tool for customizing Ubuntu images for Amazon EC2 from your local computer.
4
+
5
+ ## Install
6
+
7
+ gem install build-ubuntu-ami
8
+
9
+ ## Basic Usage
10
+
11
+ build-ubuntu-ami my_custom_script.sh
12
+
13
+ ## How it works
14
+
15
+ This program is based heavily on Eric Hammond's blog post [Creating Public AMIs Securely for EC2](http://alestic.com/2011/06/ec2-ami-security), and his shell script [alestic-git-build-ami](https://github.com/alestic/alestic-git/blob/master/bin/alestic-git-build-ami).
16
+
17
+ It works as follows:
18
+
19
+ 1. Boot an official Ubuntu EC2 instance
20
+ 2. Download and mount a copy of the official Ubuntu root volume image
21
+ 3. Run the custom user script in a chrooted environment on that image
22
+ 4. Attach an empty EBS volume
23
+ 5. Copy the customized boot image to the EBS volume
24
+ 6. Register an AMI from the customized EBS volume
25
+
26
+ Booting and logging in to a system offers many opportunities to leak secret credentials (even if you [delete them](http://alestic.com/2009/09/ec2-public-ebs-danger)). Creating an AMI from a pristine image rather than a running root volume obviates the need to remove leaked credentials.
27
+
28
+ This script does not need a private key & cert for credentials. It uses the AWS Access Key ID and Secret Access Key.
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << 'lib'
4
+ require 'build_ubuntu_ami'
5
+ require 'optparse'
6
+ require 'open-uri'
7
+
8
+ options = BuildUbuntuAmi.default_options
9
+
10
+ OptionParser.new do |opts|
11
+ opts.banner = "Usage: #{$0} [options] [path or url to user script]"
12
+ opts.on('-r', '--region=REGION', "AWS region (#{options[:region]})") { |o| options[:region] = o }
13
+ opts.on('-f', '--flavor=FLAVOR', "AWS instance type (#{options[:flavor]})") { |o| options[:flavor] = o }
14
+ opts.on('-b', '--brand=BRAND', "brand prefix (#{options[:brand]})") { |o| options[:brand] = o }
15
+ opts.on('-s', '--size=SIZE', "Size of root volume in GB (#{options[:size]})") { |o| options[:size] = o }
16
+ opts.on('-c', '--codename=CODENAME', "Ubuntu release codename (#{options[:codename]})") { |o| options[:codename] = o }
17
+ opts.on('-k', '--key_name=KEY_NAME', "EC2 key name for scratch AMI (#{options[:key_name]})") { |o| options[:key_name] = o }
18
+ opts.on('-g', '--group=GROUP', "EC2 security group for scratch AMI (#{options[:group]})") { |o| options[:group] = o }
19
+
20
+ opts.on('-h', '--help', 'Show this message') { puts opts; exit }
21
+ end.parse!
22
+
23
+ unless ARGV[0]
24
+ message = %{
25
+ Must specify a custom user script (file or url)
26
+ See https://github.com/kickstarter/build-ubuntu-ami/tree/master/examples
27
+ }.strip
28
+ raise OptionParser::MissingArgument.new(message)
29
+ end
30
+ options[:custom_user_script] = open(ARGV[0]).read
31
+
32
+ BuildUbuntuAmi.new(options).build!
@@ -0,0 +1,60 @@
1
+ #!/bin/bash -ex
2
+ # User data script
3
+
4
+ # Log output to the console, syslog, and /var/log/user-data.log
5
+ exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1
6
+
7
+ # Download and mount the clean image
8
+ image=/mnt/<%= image_name %>.img
9
+ imagedir=/mnt/<%= image_name %>-cloudimg-<%= arch %>
10
+ wget -qO- <%= image_url %> |
11
+ tar xzf - -C /mnt
12
+ mkdir -p $imagedir
13
+ mount -o loop $image $imagedir
14
+
15
+ # Allow network access from chroot environment
16
+ cp /etc/resolv.conf $imagedir/etc/
17
+
18
+ # Upgrade and install packages on the target file system
19
+ chroot $imagedir mount -t proc none /proc
20
+ #sudo chroot $imagedir mount -t devpts none /dev/pts
21
+ cat <<EOF | tee $imagedir/usr/sbin/policy-rc.d > /dev/null
22
+ #!/bin/sh
23
+ exit 101
24
+ EOF
25
+ chmod 755 $imagedir/usr/sbin/policy-rc.d
26
+ chroot $imagedir apt-get update
27
+ chroot $imagedir apt-get dist-upgrade -y
28
+
29
+ # RUN CUSTOM USER SCRIPT
30
+ cat <<CUSTOM_SCRIPT_EOF > $imagedir/tmp/custom_user_script
31
+ <%= custom_user_script %>
32
+ CUSTOM_SCRIPT_EOF
33
+
34
+ chmod +x $imagedir/tmp/custom_user_script
35
+ chroot $imagedir /tmp/custom_user_script
36
+ # END CUSTOM USER SCRIPT
37
+
38
+ # Clean up chroot environment
39
+ chroot $imagedir umount /proc
40
+ #chroot $imagedir umount /dev/pts
41
+ rm -f $imagedir/usr/sbin/policy-rc.d
42
+
43
+ # Wait for EBS volume to be attached
44
+ dev=<%= ebs_device %>
45
+ while [ ! -e $dev ]; do sleep 1; done
46
+
47
+ # Format and mount the EBS volume
48
+ yes | sudo mkfs.ext4 -L cloudimg-rootfs $dev
49
+ ebsimagedir=$imagedir-ebs
50
+ mkdir $ebsimagedir
51
+ mount $dev $ebsimagedir
52
+
53
+ # Copy file system from temporary rootdir to EBS volume
54
+ tar -cSf - -C $imagedir . | sudo tar xvf - -C $ebsimagedir
55
+
56
+ umount $imagedir
57
+ umount $ebsimagedir
58
+
59
+ # Shutdown to signal success
60
+ shutdown -h now
@@ -0,0 +1,157 @@
1
+ require 'open-uri'
2
+ require 'fog'
3
+ require 'erb'
4
+
5
+ class BuildUbuntuAmi
6
+ USER_DATA = File.read(File.join(File.dirname(__FILE__),'..','data','user_data.sh.erb'))
7
+
8
+ attr_accessor :region, :flavor, :brand, :size, :codename, :key_name, :group,
9
+ :custom_user_script, :now, :server, :volume, :snapshot, :arch,
10
+ :canonical_ami, :kernel
11
+
12
+ def self.default_options
13
+ {
14
+ :region => 'us-east-1',
15
+ :flavor => 'm1.small',
16
+ :brand => 'My',
17
+ :size => 20,
18
+ :codename => 'lucid',
19
+ :key_name => 'default',
20
+ :group => 'default',
21
+ :arch => 'amd64',
22
+ :kernel => nil
23
+ }
24
+ end
25
+
26
+ def initialize(opts={})
27
+ opts = self.class.default_options.merge(opts)
28
+ puts "Configuration:"
29
+ (self.class.default_options.keys - [:kernel]).each do |attr|
30
+ puts " #{attr}: #{opts[attr]}"
31
+ self.send("#{attr}=", opts[attr])
32
+ end
33
+ self.custom_user_script = opts[:custom_user_script]
34
+
35
+ find_canonical_ubuntu_ami
36
+ puts " canonical ami: #{canonical_ami}"
37
+ puts " kernel: #{kernel}"
38
+
39
+ @now = Time.now.strftime('%Y%m%d-%H%M')
40
+ puts " description: #{description}"
41
+ end
42
+
43
+ # Return the Ubuntu AMI with the architecture for +flavor+
44
+ def find_canonical_ubuntu_ami
45
+ url = "http://uec-images.ubuntu.com/query/#{codename}/server/released.current.txt"
46
+ data = open(url).read.split("\n").map{|l| l.split}.detect do |ary|
47
+ ary[4] == 'ebs' &&
48
+ ary[5] == arch &&
49
+ ary[6] == region &&
50
+ ary[9] == 'paravirtual'
51
+ end
52
+
53
+ self.canonical_ami = data[7]
54
+ self.kernel ||= data[8]
55
+ end
56
+
57
+ def image_arch
58
+ case arch
59
+ when 'amd64'
60
+ 'x86_64'
61
+ else
62
+ arch
63
+ end
64
+
65
+ end
66
+
67
+ def description
68
+ "#{brand}-#{codename}-#{arch}-#{now}"
69
+ end
70
+
71
+ def ebs_device
72
+ '/dev/sdi'
73
+ end
74
+
75
+ def root_device
76
+ '/dev/sda1'
77
+ end
78
+
79
+ def block_device_mapping
80
+ [{ "DeviceName" => root_device, "SnapshotId" => snapshot.id, "VolumeSize" => size, "DeleteOnTermination" => true }]
81
+ end
82
+
83
+ def image_name
84
+ "#{codename}-server-cloudimg-#{arch}"
85
+ end
86
+
87
+ def image_url
88
+ "http://uec-images.ubuntu.com/#{codename}/current/#{image_name}.tar.gz"
89
+ end
90
+
91
+ def launch_server!
92
+ puts "Launching server..."
93
+ self.server = Fog::Compute[:aws].servers.create({
94
+ :flavor_id => flavor,
95
+ :image_id => canonical_ami,
96
+ :groups => [group],
97
+ :key_name => key_name,
98
+ :user_data => user_data
99
+ })
100
+ server.wait_for { ready? }
101
+ puts "Launched #{server.id}; #{server.dns_name}; waiting for it to be available."
102
+ end
103
+
104
+ def launch_volume!
105
+ self.volume = server.volumes.create(:size => size, :device => ebs_device)
106
+ puts "Attaching volume #{volume.id}"
107
+ volume.wait_for { state == 'in-use' }
108
+ end
109
+
110
+ def snapshot_description
111
+ "#{description} root volume"
112
+ end
113
+
114
+ def take_snapshot!
115
+ puts "Waiting for volume to detach..."
116
+ volume.wait_for { state == 'available' }
117
+ puts "Taking snapshot #{snapshot_description} from #{volume.id}"
118
+ self.snapshot = volume.snapshots.create(:description => snapshot_description)
119
+ puts "Creating snapshot #{snapshot.id}"
120
+ snapshot.wait_for { ready? }
121
+ end
122
+
123
+ def user_data
124
+ @user_data ||= USER_DATA
125
+ ERB.new(@user_data).result(binding)
126
+ end
127
+
128
+ def cleanup!
129
+ puts "Deleting #{volume.id}"
130
+ volume.destroy
131
+ puts "Terminating #{server.id}"
132
+ server.destroy
133
+ end
134
+
135
+
136
+ def build!
137
+ launch_server!
138
+ launch_volume!
139
+
140
+ puts "waiting for user_data to complete and server to shut down..."
141
+ puts "Follow along by running:"
142
+ puts " ssh -l #{server.username} #{server.dns_name} 'tail -f /var/log/user.log'"
143
+ server.wait_for { state == 'stopped' }
144
+
145
+ puts "Detaching volume"
146
+ volume.server = nil
147
+
148
+ take_snapshot!
149
+
150
+ # Register AMI
151
+ res = Fog::Compute[:aws].register_image(description, description, root_device, block_device_mapping, 'KernelId' => kernel, 'Architecture' => image_arch)
152
+ puts "Registered imageId #{res.body['imageId']}"
153
+
154
+ cleanup!
155
+
156
+ end
157
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: build-ubuntu-ami
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Aaron Suggs
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: fog
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.3.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.3.0
30
+ description: Create customized ubuntu AMIs the hard (secure) way.
31
+ email: aaron@ktheory.com
32
+ executables:
33
+ - build-ubuntu-ami
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - bin/build-ubuntu-ami
38
+ - lib/build_ubuntu_ami.rb
39
+ - data/user_data.sh.erb
40
+ - LICENSE
41
+ - README.md
42
+ homepage: http://github.com/kickstarter/build-ubuntu-ami
43
+ licenses: []
44
+ post_install_message:
45
+ rdoc_options:
46
+ - --charset=UTF-8
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: 1.3.5
61
+ requirements: []
62
+ rubyforge_project:
63
+ rubygems_version: 1.8.23
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: Securely create a customized Ubuntu Amazon Machine Image.
67
+ test_files: []