aws-ec2 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,157 @@
1
+ require "byebug"
2
+
3
+
4
+ class AwsEc2::Create
5
+ class Params
6
+ include AwsEc2::TemplateHelper
7
+
8
+ def initialize(options)
9
+ @options = options
10
+ end
11
+
12
+ # deep_symbolize_keys is ran at the very end only.
13
+ # up until that point we're dealing with String keys.
14
+ def generate
15
+ cleanup
16
+ params = load_profiles(profile_name)
17
+ decorate_params(params)
18
+ normalize_launch_template(params).deep_symbolize_keys
19
+ end
20
+
21
+ def decorate_params(params)
22
+ upsert_name_tag!(params)
23
+ replace_runtime_options!(params)
24
+ params
25
+ end
26
+
27
+ # Expose a list of runtime params that are convenient. Try to limit the
28
+ # number of options from the cli to keep tool simple. Most options can
29
+ # be easily control through profile files. The runtime options that are
30
+ # very convenient to have at the CLI are modified here.
31
+ def replace_runtime_options!(params)
32
+ params["image_id"] = @options[:source_ami_id] if @options[:source_ami_id]
33
+ params
34
+ end
35
+
36
+ def cleanup
37
+ FileUtils.rm_f("/tmp/aws-ec2/user-data.txt")
38
+ end
39
+
40
+ # Adds instance ec2 tag if not already provided
41
+ def upsert_name_tag!(params)
42
+ specs = params["tag_specifications"] || []
43
+
44
+ # insert an empty spec placeholder if one not found
45
+ spec = specs.find do |s|
46
+ s["resource_type"] == "instance"
47
+ end
48
+ unless spec
49
+ spec = {
50
+ "resource_type" => "instance",
51
+ "tags" => []
52
+ }
53
+ specs << spec
54
+ end
55
+ # guaranteed there's a tag_specifications with resource_type instance at this point
56
+
57
+ tags = spec["tags"] || []
58
+
59
+ unless tags.map { |t| t["key"] }.include?("Name")
60
+ tags << { "key" => "Name", "value" => @options[:name] }
61
+ end
62
+
63
+ specs = specs.map do |s|
64
+ # replace the name tag value
65
+ if s["resource_type"] == "instance"
66
+ {
67
+ "resource_type" => "instance",
68
+ "tags" => tags
69
+ }
70
+ else
71
+ s
72
+ end
73
+ end
74
+
75
+ params["tag_specifications"] = specs
76
+ params
77
+ end
78
+
79
+ # Allow adding launch template as a simple string.
80
+ #
81
+ # Standard structure:
82
+ # {
83
+ # launch_template: { launch_template_name: "TestLaunchTemplate" },
84
+ # }
85
+ #
86
+ # Simple string:
87
+ # {
88
+ # launch_template: "TestLaunchTemplate",
89
+ # }
90
+ #
91
+ # When launch_template is a simple String it will get transformed to the
92
+ # standard structure.
93
+ def normalize_launch_template(params)
94
+ if params["launch_template"].is_a?(String)
95
+ launch_template_identifier = params["launch_template"]
96
+ launch_template = if launch_template_identifier =~ /^lt-/
97
+ { "launch_template_id" => launch_template_identifier }
98
+ else
99
+ { "launch_template_name" => launch_template_identifier }
100
+ end
101
+ params["launch_template"] = launch_template
102
+ end
103
+ params
104
+ end
105
+
106
+ # Hard coded sensible defaults.
107
+ # Can be overridden easily with profiles
108
+ def defaults
109
+ {
110
+ max_count: 1,
111
+ min_count: 1,
112
+ }
113
+ end
114
+
115
+ def load_profiles(profile_name)
116
+ return @profile_params if @profile_params
117
+
118
+ profile_file = "#{AwsEc2.root}/profiles/#{profile_name}.yml"
119
+ base_path = File.dirname(profile_file)
120
+ default_file = "#{base_path}/default.yml"
121
+
122
+ params_exit_check!(profile_file, default_file)
123
+
124
+ params = File.exist?(profile_file) ?
125
+ load_profile(profile_file) :
126
+ load_profile(default_file)
127
+ @profile_params = params
128
+ end
129
+
130
+ def params_exit_check!(profile_file, default_file)
131
+ return if File.exist?(profile_file) or File.exist?(default_file)
132
+
133
+ puts "Unable to find a #{profile_file} or #{default_file} profile file."
134
+ puts "Please double check."
135
+ exit # EXIT HERE
136
+ end
137
+
138
+ def load_profile(file)
139
+ return {} unless File.exist?(file)
140
+
141
+ puts "Using profile: #{file}"
142
+ data = YAML.load(erb_result(file))
143
+ data ? data : {} # in case the file is empty
144
+ data.has_key?("run_instances") ? data["run_instances"] : data
145
+ end
146
+
147
+ def profile_name
148
+ # allow user to specify the path also
149
+ if @options[:profile] && File.exist?(@options[:profile])
150
+ profile = File.basename(@options[:profile], '.yml')
151
+ end
152
+
153
+ # conventional profile is the name of the ec2 instance
154
+ profile || @options[:profile] || @options[:name]
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,30 @@
1
+ require 'dotenv'
2
+ require 'pathname'
3
+
4
+ class AwsEc2::Dotenv
5
+ class << self
6
+ def load!
7
+ ::Dotenv.load(*dotenv_files)
8
+ end
9
+
10
+ # dotenv files will load the following files, starting from the bottom. The first value set (or those already defined in the environment) take precedence:
11
+
12
+ # - `.env` - The Original®
13
+ # - `.env.development`, `.env.test`, `.env.production` - Environment-specific settings.
14
+ # - `.env.local` - Local overrides. This file is loaded for all environments _except_ `test`.
15
+ # - `.env.development.local`, `.env.test.local`, `.env.production.local` - Local overrides of environment-specific settings.
16
+ #
17
+ def dotenv_files
18
+ [
19
+ root.join(".env.#{AwsEc2.env}.local"),
20
+ (root.join(".env.local") unless AwsEc2.env == "test"),
21
+ root.join(".env.#{AwsEc2.env}"),
22
+ root.join(".env")
23
+ ].compact
24
+ end
25
+
26
+ def root
27
+ AwsEc2.root || Pathname.new(ENV["AWS_EC2_ROOT"] || Dir.pwd)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,11 @@
1
+ Examples:
2
+
3
+ $ aws-ec2 ami myrubyami --profile ruby --noop
4
+
5
+ The launches an EC2 instance with using the profile running it's user-data script. An ami creation script is appended to the end of the user-data script. The ami creation script uses the AWS CLI `aws ec2 create-image` command to create an AMI. It is useful to include to timestamp as a part of the ami name with the date command.
6
+
7
+ $ aws-ec2 ami $(date "+ruby-2.5.0_%Y-%m-%d-%H-%M") --profile ruby --noop
8
+
9
+ Note, it is recommended to use the `set -e` option in your user-data script so that if the script fails, the ami creation script is never reached and instance is left behind so you can debug.
10
+
11
+ The instance also automatically gets terminated and cleaned up.
@@ -1,11 +1,11 @@
1
1
  Examples:
2
2
 
3
- $ aws-ec2 create my-instance
3
+ $ aws-ec2 create my-instance
4
4
 
5
5
  If you want to create an ami at the end of of a successful user-data script run you can use the `--ami` option. Example:
6
6
 
7
- $ aws-ec2 create my-instance --ami myname
7
+ $ aws-ec2 create my-instance --ami myname
8
8
 
9
9
  To see the snippet of code that gets added to the user-data script you can use the aws-ec2 userdata command.
10
10
 
11
- $ aws-ec2 userdata myscript --ami myname
11
+ $ aws-ec2 userdata myscript --ami myname
@@ -1,13 +1,13 @@
1
1
  Displays the generated user data script. Useful for debugging since ERB can be ran on the user-data scripts.
2
2
 
3
- Given a user data script in profiles/user-data/myscript.sh, run:
3
+ Given a user data script in app/user-data/myscript.sh, run:
4
4
 
5
- $ aws-ec2 userdata myscript
5
+ $ aws-ec2 userdata myscript
6
6
 
7
7
  You can have an ami creation snippet of code added to the end of the user data script with the `--ami` option.
8
8
 
9
- $ aws-ec2 userdata myscript --ami myname
9
+ $ aws-ec2 userdata myscript --ami myname
10
10
 
11
11
  If you want to include a timestamp in the name you can use this:
12
12
 
13
- $ aws-ec2 userdata myscript --ami '`date "+myname_%Y-%m-%d-%H-%M"`'
13
+ $ aws-ec2 userdata myscript --ami '`date "+myname_%Y-%m-%d-%H-%M"`'
@@ -0,0 +1,32 @@
1
+ require 'yaml'
2
+
3
+ module AwsEc2
4
+ class Hook
5
+ def initialize(options={})
6
+ @options = options
7
+ end
8
+
9
+ def run(name)
10
+ return if @options[:noop]
11
+ return unless hooks[name]
12
+ command = hooks[name]
13
+ puts "Running hook #{name}: #{command}"
14
+ sh(command)
15
+ end
16
+
17
+ def hooks
18
+ hooks_path = "#{AwsEc2.root}/config/hooks.yml"
19
+ data = File.exist?(hooks_path) ? YAML.load_file(hooks_path) : {}
20
+ data ? data : {} # in case the file is empty
21
+ end
22
+
23
+ def sh(command)
24
+ puts "=> #{command}".colorize(:cyan)
25
+ system(command)
26
+ end
27
+
28
+ def self.run(name, options={})
29
+ Hook.new(options).run(name.to_s)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,26 @@
1
+ module AwsEc2
2
+ class Script
3
+ def initialize(options={})
4
+ @options = options
5
+ end
6
+
7
+ def auto_terminate
8
+ @ami_name = @options[:ami_name]
9
+ load_template("auto_terminate.sh")
10
+ end
11
+
12
+ def create_ami
13
+ # set some variables for the template
14
+ @ami_name = @options[:ami_name]
15
+ @region = `aws configure get region`.strip rescue 'us-east-1'
16
+ load_template("ami_creation.sh")
17
+ end
18
+
19
+ private
20
+ def load_template(name)
21
+ template = IO.read(File.expand_path("../scripts/#{name}", __FILE__))
22
+ text = ERB.new(template, nil, "-").result(binding)
23
+ "#" * 60 + "\n#{text}"
24
+ end
25
+ end
26
+ end
@@ -1,27 +1,35 @@
1
1
  #!/bin/bash -exu
2
-
3
- # Configure aws cli in case it is not yet configured
4
- mkdir -p /home/ec2-user/.aws
5
- if [ ! -f /home/ec2-user/.aws/config ]; then
6
- cat >/home/ec2-user/.aws/config <<EOL
2
+ # The shebang line is here in case there's is currently an empty user-data script.
3
+ # It wont hurt if already there.
4
+ ######################################
5
+ # ami_creation.sh: added to the end of user-data automatically.
6
+ function configure_aws_cli() {
7
+ local home_dir=$1
8
+ # Configure aws cli in case it is not yet configured
9
+ mkdir -p $home_dir/.aws
10
+ if [ ! -f $home_dir/.aws/config ]; then
11
+ cat >$home_dir/.aws/config <<EOL
7
12
  [default]
8
- region = <%= region %>
13
+ region = <%= @region %>
9
14
  output = json
10
15
  EOL
11
- fi
16
+ fi
17
+ }
12
18
 
13
- # The aws ec2 create-image command below reboots the instance.
14
- # So before rebooting the instance, schedule a job to terminate the instance
15
- # in 20 mins after the machine has rebooted
16
- cat >~/terminate-myself.sh <<EOL
17
- #!/bin/bash -exu
18
- INSTANCE_ID=$(wget -q -O - http://169.254.169.254/latest/meta-data/instance-id)
19
- aws ec2 terminate-instances --instance-ids \$INSTANCE_ID
20
- EOL
21
- at now + 1 minutes -f ~/terminate-myself.sh
19
+ configure_aws_cli /home/ec2-user
20
+ configure_aws_cli /root
21
+
22
+ echo "############################################" >> /var/log/user-data.log
23
+ echo "# Logs above is from the original AMI baking at: $(date)" >> /var/log/user-data.log
24
+ echo "# New logs below" >> /var/log/user-data.log
25
+ echo "############################################" >> /var/log/user-data.log
22
26
 
23
27
  # Create AMI Bundle
24
28
  AMI_NAME="<%= @ami_name %>"
25
29
  INSTANCE_ID=$(wget -q -O - http://169.254.169.254/latest/meta-data/instance-id)
26
30
  REGION=$(aws configure get region)
31
+ # Note this will cause the instance to reboot. Not using the --no-reboot flag
32
+ # to ensure consistent AMI creation.
33
+ SOURCE_AMI_ID=$(wget -q -O - http://169.254.169.254/latest/meta-data/ami-id)
34
+ echo $SOURCE_AMI_ID > /var/log/source-ami-id.txt
27
35
  aws ec2 create-image --name $AMI_NAME --instance-id $INSTANCE_ID --region $REGION
@@ -0,0 +1,95 @@
1
+ #!/bin/bash -exu
2
+ # The shebang line is here in case there's is currently an empty user-data script.
3
+ # It wont hurt if already there.
4
+ ##################
5
+ # auto_terminate.sh script
6
+ # When creating an AMI, a aws ec2 create-image command is added to the end of
7
+ # the user-data script. Creating AMIs prevent the script going any further.
8
+ #
9
+ # To get around this the this is script is added before that happens.
10
+ #
11
+ # https://stackoverflow.com/questions/27920806/how-to-avoid-heredoc-expanding-variables
12
+ cat >/root/terminate-myself.sh << 'EOL'
13
+ #!/bin/bash -exu
14
+
15
+ # install jq dependencies
16
+ function install_jq() {
17
+ if ! type jq > /dev/null ; then
18
+ wget "https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64"
19
+ mv jq-linux64 /usr/local/bin/jq
20
+ chmod a+x /usr/local/bin/jq
21
+ fi
22
+ }
23
+
24
+ function configure_aws_cli() {
25
+ local home_dir=$1
26
+ # Configure aws cli in case it is not yet configured
27
+ mkdir -p $home_dir/.aws
28
+ if [ ! -f $home_dir/.aws/config ]; then
29
+ EC2_AVAIL_ZONE=`curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone`
30
+ EC2_REGION="`echo \"$EC2_AVAIL_ZONE\" | sed -e 's:\([0-9][0-9]*\)[a-z]*\$:\\1:'`"
31
+ cat >$home_dir/.aws/config <<CONFIGURE_AWS_CLI
32
+ [default]
33
+ region = $EC2_REGION
34
+ output = json
35
+ CONFIGURE_AWS_CLI
36
+ fi
37
+ }
38
+
39
+ function terminate_instance() {
40
+ aws ec2 terminate-instances --instance-ids $INSTANCE_ID
41
+ }
42
+
43
+ # on-demand instance example:
44
+ # $ aws ec2 describe-instances --instance-ids i-09482b1a6e330fbf7 | jq '.Reservations[].Instances[].SpotInstanceRequestId'
45
+ # null
46
+ # spot instance example:
47
+ # $ aws ec2 describe-instances --instance-ids i-08318bb7f33c216bd | jq '.Reservations[].Instances[].SpotInstanceRequestId'
48
+ # "sir-dzci5wsh"
49
+ function cancel_spot_request() {
50
+ aws ec2 cancel-spot-instance-requests --spot-instance-request-ids $SPOT_INSTANCE_REQUEST_ID
51
+ }
52
+
53
+ ###
54
+ # program starts here
55
+ ###
56
+ export PATH=/usr/local/bin:$PATH
57
+ install_jq
58
+ configure_aws_cli /root
59
+
60
+ AMI_NAME=$1
61
+ if [ $AMI_NAME != "NO-WAIT" ]; then
62
+ # wait for the ami to be successfully created before terminating the instance
63
+ # https://docs.aws.amazon.com/cli/latest/reference/ec2/wait/image-available.html
64
+ # It will poll every 15 seconds until a successful state has been reached. This will exit with a return code of 255 after 40 failed checks.
65
+ # so it'll wait for 10 mins max
66
+ aws ec2 wait image-available --filters "Name=name,Values=$AMI_NAME" --owners self
67
+ fi
68
+
69
+
70
+ INSTANCE_ID=$(wget -q -O - http://169.254.169.254/latest/meta-data/instance-id)
71
+ SPOT_INSTANCE_REQUEST_ID=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID | jq -r '.Reservations[].Instances[].SpotInstanceRequestId')
72
+
73
+ # Remove this script so it is only allowed to be ran once ever
74
+ # Or else whenenver we launch the AMI, it will kill itself
75
+ rm -f /root/terminate-myself.sh
76
+ grep -v terminate-myself /etc/rc.d/rc.local > /etc/rc.d/rc.local.tmp
77
+ mv /etc/rc.d/rc.local.tmp /etc/rc.d/rc.local
78
+
79
+ if [ -n "$SPOT_INSTANCE_REQUEST_ID" ]; then
80
+ cancel_spot_request
81
+ fi
82
+ terminate_instance
83
+ EOL
84
+ chmod a+x /root/terminate-myself.sh
85
+
86
+ <% if @options[:auto_terminate] %>
87
+ <% if @options[:ami_name] %>
88
+ # schedule termination upon reboot
89
+ chmod +x /etc/rc.d/rc.local
90
+ echo "/root/terminate-myself.sh <%= @options[:ami_name] %> >> /var/log/terminate-myself.log 2>&1" >> /etc/rc.d/rc.local
91
+ <% else %>
92
+ # terminate immediately
93
+ /root/terminate-myself.sh NO-WAIT >> /var/log/terminate-myself.log 2>&1
94
+ <% end %>
95
+ <% end %>