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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/Gemfile.lock +84 -52
- data/README.md +3 -4
- data/aws-ec2.gemspec +3 -0
- data/lib/aws-ec2.rb +7 -4
- data/lib/aws_ec2/ami.rb +7 -8
- data/lib/aws_ec2/cli.rb +12 -10
- data/lib/aws_ec2/command.rb +13 -0
- data/lib/aws_ec2/compile_scripts.rb +30 -0
- data/lib/aws_ec2/core.rb +4 -1
- data/lib/aws_ec2/create.rb +14 -84
- data/lib/aws_ec2/create/params.rb +157 -0
- data/lib/aws_ec2/dotenv.rb +30 -0
- data/lib/aws_ec2/help/ami.md +11 -0
- data/lib/aws_ec2/help/create.md +3 -3
- data/lib/aws_ec2/help/user_data.md +4 -4
- data/lib/aws_ec2/hook.rb +32 -0
- data/lib/aws_ec2/script.rb +26 -0
- data/lib/aws_ec2/scripts/ami_creation.sh +24 -16
- data/lib/aws_ec2/scripts/auto_terminate.sh +95 -0
- data/lib/aws_ec2/template_helper.rb +47 -12
- data/lib/aws_ec2/template_helper/ami_helper.rb +23 -0
- data/lib/aws_ec2/template_helper/partial_helper.rb +71 -0
- data/lib/aws_ec2/version.rb +1 -1
- data/spec/fixtures/demo_project/config/test.yml +9 -0
- data/spec/fixtures/demo_project/profiles/default.yml +33 -0
- data/spec/lib/cli_spec.rb +9 -4
- data/spec/spec_helper.rb +4 -3
- metadata +57 -8
- data/example/profiles/spot/default.yml +0 -14
- data/example/profiles/spot/dev.yml +0 -15
- data/lib/aws_ec2/help/spot.md +0 -3
- data/lib/aws_ec2/spot.rb +0 -81
- data/lib/aws_ec2/user_data.rb +0 -17
- data/lib/aws_ec2/util.rb +0 -64
@@ -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.
|
data/lib/aws_ec2/help/create.md
CHANGED
@@ -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
|
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"`'
|
data/lib/aws_ec2/hook.rb
ADDED
@@ -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
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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 %>
|