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