aws-ec2 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 %>