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.
@@ -1,17 +1,35 @@
1
1
  require "base64"
2
2
  require "erb"
3
+ require "active_support/all"
3
4
 
4
5
  module AwsEc2
5
6
  module TemplateHelper
7
+ # auto load all the template_helpers
8
+ template_helper_path = File.expand_path("../template_helper", __FILE__)
9
+ Dir.glob("#{template_helper_path}/*").each do |path|
10
+ next if File.directory?(path)
11
+ filename = File.basename(path, '.rb')
12
+ class_name = filename.classify
13
+ instance_eval do
14
+ autoload class_name.to_sym, "aws_ec2/template_helper/#{filename}"
15
+ include const_get(class_name)
16
+ end
17
+ end
18
+
6
19
  def user_data(name, base64=true)
7
20
  # allow user to specify the path also
8
21
  if File.exist?(name)
9
22
  name = File.basename(name) # normalize name, change path to name
10
23
  end
11
24
  name = File.basename(name, '.sh')
12
- path = "#{root}/profiles/user-data/#{name}.sh"
25
+ path = "#{AwsEc2.root}/app/user-data/#{name}.sh"
13
26
  result = erb_result(path)
14
- result = append_ami_creation(result)
27
+ result = append_scripts(result)
28
+
29
+ # save the unencoded user-data script for easy debugging
30
+ temp_path = "/tmp/aws-ec2/user-data.txt"
31
+ FileUtils.mkdir_p(File.dirname(temp_path))
32
+ IO.write(temp_path, result)
15
33
 
16
34
  base64 ? Base64.encode64(result).strip : result
17
35
  end
@@ -30,21 +48,36 @@ module AwsEc2
30
48
  end
31
49
 
32
50
  private
33
- def append_ami_creation(user_data)
34
- ami = @options[:ami]
35
-
36
- if ami
37
- # assuming that the user-data script is a bash script here for simplicity
38
- # TODO: add support for other types of scripts
39
- # might be able to do this by wrapping all scripts in cloud-init
40
- ami_creation_snippet = AwsEc2::Ami.new(ami).user_data_snippet
41
- user_data += ami_creation_snippet
42
- end
51
+ def append_scripts(user_data)
52
+ # assuming user-data script is a bash script for simplicity
53
+ script = AwsEc2::Script.new(@options)
54
+ user_data += script.auto_terminate if @options[:auto_terminate]
55
+ user_data += script.create_ami if @options[:ami_name]
43
56
  user_data
44
57
  end
45
58
 
59
+ # Load custom helper methods from the project repo
60
+ def load_custom_helpers
61
+ Dir.glob("#{AwsEc2.root}/app/helpers/**/*_helper.rb").each do |path|
62
+ filename = path.sub(%r{.*/},'').sub('.rb','')
63
+ module_name = filename.classify
64
+
65
+ require path
66
+ self.class.send :include, module_name.constantize
67
+ end
68
+
69
+ end
70
+
46
71
  def erb_result(path)
72
+ load_custom_helpers
47
73
  template = IO.read(path)
74
+
75
+ # Allow a way to bypass the custom ERB error handling in case
76
+ # the error is in the lambdagem code.
77
+ if ENV['DEBUG']
78
+ return ERB.new(template, nil, "-").result(binding)
79
+ end
80
+
48
81
  begin
49
82
  ERB.new(template, nil, "-").result(binding)
50
83
  rescue Exception => e
@@ -71,6 +104,8 @@ module AwsEc2
71
104
  printf("%#{spacing}d %s\n", line_number, line_content)
72
105
  end
73
106
  end
107
+
108
+ puts "\nIf the this error does not make sense and the error is not in the ERB template. Run the command again with DEBUG=1 to show the full lambdagem backtrace"
74
109
  exit 1 unless ENV['TEST']
75
110
  end
76
111
  end
@@ -0,0 +1,23 @@
1
+ module AwsEc2::TemplateHelper::AmiHelper
2
+ include AwsEc2::AwsServices
3
+
4
+ # Example:
5
+ #
6
+ # latest_ami("ruby-2.5.0_*") => ami-122
7
+ #
8
+ # Equivalent aws cli test command:
9
+ #
10
+ # $ aws ec2 describe-images --owners self --filters="Name=name,Values=ruby-2.5.0_*" | jq '.Images | length'
11
+ #
12
+ # Returns latest ami ami
13
+ def latest_ami(query, owners=["self"])
14
+ images = ec2.describe_images(
15
+ owners: owners,
16
+ filters: [
17
+ {name: "name", values: [query]}
18
+ ]
19
+ ).images
20
+ image = images.sort_by(&:name).reverse.first
21
+ image.image_id
22
+ end
23
+ end
@@ -0,0 +1,71 @@
1
+ module AwsEc2::TemplateHelper::PartialHelper
2
+ def partial_exist?(path)
3
+ path = partial_path_for(path)
4
+ path = auto_add_format(path)
5
+ path && File.exist?(path)
6
+ end
7
+
8
+ # The partial's path is a relative path given without the extension and
9
+ #
10
+ # Example:
11
+ # Given: file in app/partials/mypartial.sh
12
+ # The path should be: mypartial
13
+ #
14
+ # If the user specifies the extension then use that instead of auto-adding
15
+ # the detected format.
16
+ def partial(path,vars={}, options={})
17
+ path = partial_path_for(path)
18
+ path = auto_add_format(path)
19
+
20
+ result = erb_result(path)
21
+ result = indent(result, options[:indent]) if options[:indent]
22
+ if options[:indent]
23
+ # Add empty line at beginning because empty lines gets stripped during
24
+ # processing anyway. This allows the user to call partial without having
25
+ # to put the partial call at very beginning of the line.
26
+ # This only should happen if user is using indent option.
27
+ ["\n", result].join("\n")
28
+ else
29
+ result
30
+ end
31
+ end
32
+
33
+ # add indentation
34
+ def indent(text, indentation_amount)
35
+ text.split("\n").map do |line|
36
+ " " * indentation_amount + line
37
+ end.join("\n")
38
+ end
39
+
40
+ private
41
+ def partial_path_for(path)
42
+ "#{AwsEc2.root}/app/partials/#{path}"
43
+ end
44
+
45
+ def auto_add_format(path)
46
+ # Return immediately if user provided explicit extension
47
+ extension = File.extname(path) # current extension
48
+ return path if !extension.empty?
49
+
50
+ # Else let's auto detect
51
+ paths = Dir.glob("#{path}.*")
52
+
53
+ if paths.size == 1 # non-ambiguous match
54
+ return paths.first
55
+ end
56
+
57
+ if paths.size > 1 # ambiguous match
58
+ puts "ERROR: Multiple possible partials found:".colorize(:red)
59
+ paths.each do |path|
60
+ puts " #{path}"
61
+ end
62
+ puts "Please specify an extension in the name to remove the ambiguity.".colorize(:green)
63
+ exit 1
64
+ end
65
+
66
+ # Account for case when user wants to include a file with no extension at all
67
+ return path if File.exist?(path) && !File.directory?(path)
68
+
69
+ path # original path if this point is reached
70
+ end
71
+ end
@@ -1,3 +1,3 @@
1
1
  module AwsEc2
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -0,0 +1,9 @@
1
+ ---
2
+ vpc_id: vpc-123
3
+ db_subnet_group_name: default # default db subnet group associated with default vpc
4
+ subnets:
5
+ - subnet-123
6
+ - subnet-456
7
+ security_group_ids:
8
+ - sg-123 # test-single-instance
9
+ s3_bucket: my-bucket # for the user-data shared scripts
@@ -0,0 +1,33 @@
1
+ ---
2
+ # image_id: ami-97785bed # Amazon Linux AMI
3
+ image_id: ami-4fffc834 # Amazon Lambda AMI
4
+ # https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html
5
+ instance_type: t2.medium
6
+ key_name: default
7
+ max_count: 1
8
+ min_count: 1
9
+ user_data:
10
+ iam_instance_profile:
11
+ name: DevLinux
12
+ # public network settings
13
+ security_group_ids: <%= config["security_group_ids"].inspect %>
14
+ subnet_id: <%= config["subnets"].shuffle.first %>
15
+ block_device_mappings:
16
+ - device_name: /dev/xvda
17
+ ebs:
18
+ volume_size: 20
19
+ instance_market_options:
20
+ market_type: spot
21
+ # https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_LaunchTemplateSpotMarketOptionsRequest.html
22
+ # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/EC2/Types/SpotMarketOptions.html
23
+ spot_options:
24
+ max_price: "0.018" # $0.020/hr = $14.40/mo
25
+ # $0.018/hr = $12.96/mo
26
+ # valid combinations:
27
+ # spot_instance_type: persistent
28
+ # instance_interruption_behavior: hibernate
29
+ # or
30
+ # spot_instance_type: one-time
31
+ # More info: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-interruptions.html
32
+ spot_instance_type: persistent
33
+ instance_interruption_behavior: hibernate
data/spec/lib/cli_spec.rb CHANGED
@@ -7,13 +7,18 @@ require "spec_helper"
7
7
  # $ rake clean:vcr ; time rake
8
8
  describe AwsEc2::CLI do
9
9
  before(:all) do
10
- @args = "--from Tung"
10
+ @args = "--noop"
11
11
  end
12
12
 
13
13
  describe "aws-ec2" do
14
- it "should hello world" do
15
- out = execute("exe/aws-ec2 hello world #{@args}")
16
- expect(out).to include("from: Tung\nHello world")
14
+ it "create" do
15
+ out = execute("exe/aws-ec2 create server #{@args}")
16
+ expect(out).to include("Creating EC2 instance")
17
+ end
18
+
19
+ it "ami" do
20
+ out = execute("exe/aws-ec2 ami myimage #{@args}")
21
+ expect(out).to include("Creating EC2 instance")
17
22
  end
18
23
  end
19
24
  end
data/spec/spec_helper.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  ENV["TEST"] = "1"
2
-
3
- # require "simplecov"
4
- # SimpleCov.start
2
+ ENV["AWS_EC2_ENV"] = "test"
3
+ ENV["AWS_EC2_ROOT"] = "spec/fixtures/demo_project"
4
+ # Ensures aws api never called. Fixture home folder does not contain ~/.aws/credentails
5
+ ENV['HOME'] = "spec/fixtures/home"
5
6
 
6
7
  require "pp"
7
8
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aws-ec2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tung Nguyen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-01-24 00:00:00.000000000 Z
11
+ date: 2018-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -52,6 +52,48 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dotenv
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: aws-sdk-ec2
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
55
97
  - !ruby/object:Gem::Dependency
56
98
  name: bundler
57
99
  requirement: !ruby/object:Gem::Requirement
@@ -157,8 +199,6 @@ files:
157
199
  - aws-ec2.gemspec
158
200
  - example/profiles/default.yml
159
201
  - example/profiles/spot.yml
160
- - example/profiles/spot/default.yml
161
- - example/profiles/spot/dev.yml
162
202
  - example/profiles/user-data/dev.sh
163
203
  - exe/aws-ec2
164
204
  - lib/aws-ec2.rb
@@ -166,19 +206,26 @@ files:
166
206
  - lib/aws_ec2/aws_services.rb
167
207
  - lib/aws_ec2/cli.rb
168
208
  - lib/aws_ec2/command.rb
209
+ - lib/aws_ec2/compile_scripts.rb
169
210
  - lib/aws_ec2/config.rb
170
211
  - lib/aws_ec2/core.rb
171
212
  - lib/aws_ec2/create.rb
213
+ - lib/aws_ec2/create/params.rb
214
+ - lib/aws_ec2/dotenv.rb
172
215
  - lib/aws_ec2/help.rb
216
+ - lib/aws_ec2/help/ami.md
173
217
  - lib/aws_ec2/help/create.md
174
- - lib/aws_ec2/help/spot.md
175
218
  - lib/aws_ec2/help/user_data.md
219
+ - lib/aws_ec2/hook.rb
220
+ - lib/aws_ec2/script.rb
176
221
  - lib/aws_ec2/scripts/ami_creation.sh
177
- - lib/aws_ec2/spot.rb
222
+ - lib/aws_ec2/scripts/auto_terminate.sh
178
223
  - lib/aws_ec2/template_helper.rb
179
- - lib/aws_ec2/user_data.rb
180
- - lib/aws_ec2/util.rb
224
+ - lib/aws_ec2/template_helper/ami_helper.rb
225
+ - lib/aws_ec2/template_helper/partial_helper.rb
181
226
  - lib/aws_ec2/version.rb
227
+ - spec/fixtures/demo_project/config/test.yml
228
+ - spec/fixtures/demo_project/profiles/default.yml
182
229
  - spec/lib/cli_spec.rb
183
230
  - spec/spec_helper.rb
184
231
  homepage: https://github.com/tongueroo/aws-ec2
@@ -206,5 +253,7 @@ signing_key:
206
253
  specification_version: 4
207
254
  summary: Simple tool to create AWS ec2 instances
208
255
  test_files:
256
+ - spec/fixtures/demo_project/config/test.yml
257
+ - spec/fixtures/demo_project/profiles/default.yml
209
258
  - spec/lib/cli_spec.rb
210
259
  - spec/spec_helper.rb
@@ -1,14 +0,0 @@
1
- ---
2
- spot_fleet_request_config:
3
- iam_fleet_role: arn:aws:iam::123456789012:role/aws-service-role/spotfleet.amazonaws.com/AWSServiceRoleForEC2SpotFleet
4
- spot_price: '0.02'
5
- target_capacity: 1
6
- launch_specifications:
7
- - iam_instance_profile:
8
- arn: arn:aws:iam::123456789012:instance-profile/IAMProfileName
9
- image_id: ami-97785bed
10
- instance_type: t2.medium
11
- key_name: default
12
- security_groups:
13
- - group_id: sg-111
14
- subnet_id: subnet-111, subnet-222
@@ -1,15 +0,0 @@
1
- ---
2
- spot_fleet_request_config:
3
- iam_fleet_role: arn:aws:iam::123456789012:role/aws-service-role/spotfleet.amazonaws.com/AWSServiceRoleForEC2SpotFleet
4
- spot_price: '0.017'
5
- target_capacity: 1
6
- launch_specifications:
7
- - iam_instance_profile:
8
- arn: arn:aws:iam::123456789012:instance-profile/IAMProfileName
9
- image_id: ami-97785bed
10
- instance_type: t2.medium
11
- key_name: default
12
- security_groups:
13
- - group_id: sg-111
14
- subnet_id: subnet-111, subnet-222
15
- user_data: "<%= user_data("dev") %>"
@@ -1,3 +0,0 @@
1
- Examples:
2
-
3
- $ aws-ec2 spot my-request
data/lib/aws_ec2/spot.rb DELETED
@@ -1,81 +0,0 @@
1
- require 'yaml'
2
- require 'active_support/core_ext/hash'
3
-
4
- module AwsEc2
5
- class Spot
6
- include AwsServices
7
- include Util
8
-
9
- def initialize(options)
10
- @options = options
11
- end
12
-
13
- def run
14
- puts "Creating spot instance fleet request..."
15
- display_info
16
- if @options[:noop]
17
- puts "NOOP mode enabled. spot instance fleet request not created."
18
- return
19
- end
20
-
21
- resp = ec2.request_spot_fleet(params)
22
- puts "Spot instance fleet request created"
23
- end
24
-
25
- # params are main derived from profile files
26
- def params
27
- params = load_profiles("spot/#{profile_name}")
28
- params = decorate_launch_template_configs(params)
29
- params.deep_symbolize_keys
30
- end
31
-
32
- # Decorates the launch_template_configs:
33
- #
34
- # * Ensure that a launch template version is set
35
- # * Sets the ec2 tag name
36
- def decorate_launch_template_configs(params)
37
- launch_template_configs = params["spot_fleet_request_config"]["launch_template_configs"]
38
- return params unless launch_template_configs
39
-
40
- # Assume only one launch_template_configs
41
- # TODO: add support for multiple launch_template_configs
42
- config = launch_template_configs.first
43
- spec = config["launch_template_specification"]
44
- version = spec["version"]
45
- unless version
46
- latest_version = latest_version(spec["launch_template_name"])
47
- end
48
-
49
- config["launch_template_specification"]["version"] ||= latest_version
50
-
51
- # TODO: figure out how to override a tag for a launch template
52
- # # Sets the EC2 tag name.
53
- # # Replace all tag_specifications for simplicity.
54
- # tag_specifications = [{
55
- # resource_type: "instance",
56
- # tags: [{ key: "Name", value: @options[:name] }],
57
- # }]
58
- # config["overrides"] << ["tag_specifications"] << tag_specifications
59
-
60
- # replace all configs
61
- params["spot_fleet_request_config"]["launch_template_configs"] = [config]
62
-
63
- params
64
- end
65
-
66
- def latest_version(launch_template_name)
67
- resp = ec2.describe_launch_template_versions(launch_template_name: launch_template_name)
68
- version = resp.launch_template_versions.first
69
- version.version_number.to_s # request_spot_fleet expects this to be a String
70
- rescue Aws::EC2::Errors::InvalidLaunchTemplateNameNotFoundException => e
71
- puts e.message
72
- puts "Please double check that the launch template exists"
73
- exit
74
- end
75
-
76
- def display_info
77
- puts "Using the following parameters:"
78
- pretty_display(params)
79
- end
80
- end
81
- end