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.
- 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 %>
|