aws-ec2 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: aac747aab04913b92a6d0ca90cfba5879165e990ef388f990d644eace36da3d2
4
+ data.tar.gz: 18066ba7f63d5323f83e4b548339ea17d2ca6385b05548e406dad3673c67d4df
5
+ SHA512:
6
+ metadata.gz: 27ad69472b8f9978e6bdda165fd1b4d6ccbd67227a09776670aae71704ce458ea815541c826caf1b32dd2a8b0ab218a87ae623fbbc5f09f79fdc8cf4e81c7769
7
+ data.tar.gz: c6573808f67f7f8a9170995a9f4c55cbcba27c4da522dc5ed2b59d691cf2c91720834a4c67ef1e19f7c71f97303ea5a5c0325ba8a4010cfb14cc8934542f006d
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ InstalledFiles
7
+ _yardoc
8
+ coverage
9
+ doc/
10
+ lib/bundler/man
11
+ pkg
12
+ rdoc
13
+ spec/reports
14
+ test/tmp
15
+ test/version_tmp
16
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ This project *tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
5
+
6
+ ## [0.1.0]
7
+ - initial release
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem dependencies in aws_ec2.gemspec
4
+ gemspec
5
+
6
+ gem "codeclimate-test-reporter", group: :test, require: nil
data/Gemfile.lock ADDED
@@ -0,0 +1,88 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ aws_ec2 (0.0.1)
5
+ colorize
6
+ hashie
7
+ thor
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ celluloid (0.16.0)
13
+ timers (~> 4.0.0)
14
+ codeclimate-test-reporter (0.4.4)
15
+ simplecov (>= 0.7.1, < 1.0.0)
16
+ coderay (1.1.0)
17
+ colorize (0.7.5)
18
+ diff-lcs (1.2.5)
19
+ docile (1.1.5)
20
+ ffi (1.9.6)
21
+ formatador (0.2.5)
22
+ guard (2.10.5)
23
+ formatador (>= 0.2.4)
24
+ listen (~> 2.7)
25
+ lumberjack (~> 1.0)
26
+ nenv (~> 0.1)
27
+ pry (>= 0.9.12)
28
+ thor (>= 0.18.1)
29
+ guard-bundler (2.1.0)
30
+ bundler (~> 1.0)
31
+ guard (~> 2.2)
32
+ guard-compat (~> 1.1)
33
+ guard-compat (1.2.0)
34
+ guard-rspec (4.5.0)
35
+ guard (~> 2.1)
36
+ guard-compat (~> 1.1)
37
+ rspec (>= 2.99.0, < 4.0)
38
+ hashie (3.3.2)
39
+ hitimes (1.2.2)
40
+ listen (2.8.4)
41
+ celluloid (>= 0.15.2)
42
+ rb-fsevent (>= 0.9.3)
43
+ rb-inotify (>= 0.9)
44
+ lumberjack (1.0.9)
45
+ method_source (0.8.2)
46
+ multi_json (1.10.1)
47
+ nenv (0.1.1)
48
+ pry (0.10.1)
49
+ coderay (~> 1.1.0)
50
+ method_source (~> 0.8.1)
51
+ slop (~> 3.4)
52
+ rake (10.4.2)
53
+ rb-fsevent (0.9.4)
54
+ rb-inotify (0.9.5)
55
+ ffi (>= 0.5.0)
56
+ rspec (3.1.0)
57
+ rspec-core (~> 3.1.0)
58
+ rspec-expectations (~> 3.1.0)
59
+ rspec-mocks (~> 3.1.0)
60
+ rspec-core (3.1.7)
61
+ rspec-support (~> 3.1.0)
62
+ rspec-expectations (3.1.2)
63
+ diff-lcs (>= 1.2.0, < 2.0)
64
+ rspec-support (~> 3.1.0)
65
+ rspec-mocks (3.1.3)
66
+ rspec-support (~> 3.1.0)
67
+ rspec-support (3.1.2)
68
+ simplecov (0.9.1)
69
+ docile (~> 1.1.0)
70
+ multi_json (~> 1.0)
71
+ simplecov-html (~> 0.8.0)
72
+ simplecov-html (0.8.0)
73
+ slop (3.6.0)
74
+ thor (0.19.1)
75
+ timers (4.0.1)
76
+ hitimes
77
+
78
+ PLATFORMS
79
+ ruby
80
+
81
+ DEPENDENCIES
82
+ bundler (~> 1.3)
83
+ codeclimate-test-reporter
84
+ guard
85
+ guard-bundler
86
+ guard-rspec
87
+ rake
88
+ aws_ec2!
data/Guardfile ADDED
@@ -0,0 +1,19 @@
1
+ guard "bundler", cmd: "bundle" do
2
+ watch("Gemfile")
3
+ watch(/^.+\.gemspec/)
4
+ end
5
+
6
+ guard :rspec, cmd: "bundle exec rspec" do
7
+ require "guard/rspec/dsl"
8
+ dsl = Guard::RSpec::Dsl.new(self)
9
+
10
+ # RSpec files
11
+ rspec = dsl.rspec
12
+ watch(rspec.spec_helper) { rspec.spec_dir }
13
+ watch(rspec.spec_support) { rspec.spec_dir }
14
+ watch(rspec.spec_files)
15
+
16
+ # Ruby files
17
+ ruby = dsl.ruby
18
+ dsl.watch_spec_files_for(ruby.lib_files)
19
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Tung Nguyen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # AWS EC2 Tool
2
+
3
+ Simple tool to create AWS ec2 instances consistently with pre-configured settings. The pre-configured settings are stored in the profiles folder of the current directory.
4
+ Example:
5
+
6
+ * profiles/default.yml: default settings. Takes the lowest precedence.
7
+ * profiles/myserver.yml: myserver settings get combined with the default settings
8
+
9
+ ## Usage
10
+
11
+ ```sh
12
+ $ aws-ec2 create myserver --profile myserver
13
+ ```
14
+
15
+ In a nutshell, the profile parameters are passed to the ruby aws-sdk [AWS::EC2::Client#run_instances](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/EC2/Client.html#run_instances-instance_method) method. So you can specify any parameter you wish that is available there.
16
+
17
+ ### Convention
18
+
19
+ By convention, the profile name matches the first parameter after the create command. So the command above could be shortened to:
20
+
21
+ ```
22
+ $ aws-ec2 create myserver
23
+ ```
24
+
25
+ ## User-Data
26
+
27
+ You can provide user-data script to customize the server upon launch. The user-data scripts are under the profiles/user-data folder.
28
+
29
+ * profiles/user-data/myserver.yml
30
+
31
+ The user-data script is generated on the machine that is running the aws-ec2 command. If this is your local macosx machine, then the context is your local macosx machine is available. To see the generated user-data script, you can use the `aws userdata NAME`. Example:
32
+
33
+ * aws userdata myserver # shows a preview of the user-data script
34
+
35
+ To use the user-data script when creating an EC2 instance, you can use the helper method in the profile.
36
+
37
+ ## Spot Instance Support
38
+
39
+ Spot instance support natively supported by the AWS run_instances command. Simply add `instance_market_options` to the parameters to request for a spot instance. The available spot market options are available here:
40
+
41
+ * [https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_LaunchTemplateSpotMarketOptionsRequest.html](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_LaunchTemplateSpotMarketOptionsRequest.html)
42
+ * [https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/EC2/Types/SpotMarketOptions.html](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/EC2/Types/SpotMarketOptions.html)
43
+
44
+ ## Spot Fleet Support
45
+
46
+ Additionally, spot fleet is supported. Launching a fleet request is slighlyt more complicated but is useful if you are okay with multiple types of instances. The spot instance profile files are stored in the profiles/spot folder. Example:
47
+
48
+ * profiles/spot/default.yml: default settings. Takes the lowest precedence.
49
+ * profiles/spot/myspot.yml: myspot settings get combined with the default settings
50
+
51
+ Note the parameters structure of a spot fleet request is different from the parameter structure to run a single instance with the create command above. The profile parameters are passed to the ruby aws-sdk [AWS::EC2::Client#request_spot_fleet](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/EC2/Client.html#request_spot_fleet-instance_method) method. So you can specify any parameter you wish that is available there.
52
+
53
+ ```sh
54
+ ec2 spot myspot --profile myspot
55
+ ec2 spot myspot # same as above by convention
56
+ ```
57
+
58
+ ## More Help
59
+
60
+ ```sh
61
+ aws-ec2 create help
62
+ aws-ec2 userdata help
63
+ aws-ec2 spot help
64
+ aws-ec2 help # general help
65
+ ```
66
+
67
+ Examples are in the [example](example) folder. You will have to update settings like your subnet and security group ids.
68
+
69
+ ## Installation
70
+
71
+ ```sh
72
+ gem install aws-ec2
73
+ ```
74
+
75
+ ## Contributing
76
+
77
+ 1. Fork it
78
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
79
+ 3. Commit your changes (`git commit -am "Add some feature"`)
80
+ 4. Push to the branch (`git push origin my-new-feature`)
81
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ task :default => :spec
5
+
6
+ RSpec::Core::RakeTask.new
data/aws-ec2.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "aws_ec2/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "aws-ec2"
8
+ spec.version = AwsEc2::VERSION
9
+ spec.authors = ["Tung Nguyen"]
10
+ spec.email = ["tongueroo@gmail.com"]
11
+ spec.description = %q{Simple tool to create AWS ec2 instances consistently with pre-configured settings}
12
+ spec.summary = %q{Simple tool to create AWS ec2 instances}
13
+ spec.homepage = "https://github.com/tongueroo/aws-ec2"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "thor"
23
+ spec.add_dependency "hashie"
24
+ spec.add_dependency "colorize"
25
+
26
+ spec.add_development_dependency "bundler"
27
+ spec.add_development_dependency "byebug"
28
+ spec.add_development_dependency "rake"
29
+ spec.add_development_dependency "guard"
30
+ spec.add_development_dependency "guard-bundler"
31
+ spec.add_development_dependency "guard-rspec"
32
+ end
@@ -0,0 +1,12 @@
1
+ ---
2
+ image_id: ami-97785bed
3
+ instance_type: t2.medium
4
+ key_name: default
5
+ max_count: 1
6
+ min_count: 1
7
+ security_group_ids:
8
+ - sg-111
9
+ subnet_id: <%= %w[subnet-111 subnet-222].shuffle.first %>
10
+ user_data: "<%= user_data("dev") %>"
11
+ iam_instance_profile:
12
+ name: IAMProfileName
@@ -0,0 +1,9 @@
1
+ ---
2
+ instance_market_options:
3
+ market_type: spot
4
+ # https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_LaunchTemplateSpotMarketOptionsRequest.html
5
+ # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/EC2/Types/SpotMarketOptions.html
6
+ spot_options:
7
+ max_price: "0.02"
8
+ spot_instance_type: one-time
9
+ # instance_interruption_behavior: hibernate
@@ -0,0 +1,14 @@
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
@@ -0,0 +1,15 @@
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") %>"
@@ -0,0 +1,47 @@
1
+ #!/bin/bash -exu
2
+
3
+ exec > >(tee "/var/log/user-data.log" | logger -t user-data -s 2>/dev/console) 2>&1
4
+ export HOME=/root # user-data env runs in weird shell where user is root but HOME is not set
5
+ <% pubkey_path = "#{ENV['HOME']}/.ssh/id_rsa.pub" -%>
6
+ <% if File.exist?(pubkey_path) -%>
7
+ <% pubkey = IO.read(pubkey_path) %>
8
+ # Automatically add user's public key
9
+ echo <%= pubkey %> >> ~/.ssh/authorized_keys
10
+ echo <%= pubkey %> >> /home/ec2-user/.ssh/authorized_keys
11
+ chown ec2-user:ec2-user /home/ec2-user/.ssh/authorized_keys
12
+ <% else %>
13
+ # WARN: unable to find a ~/.ssh/id_rsa.pub locally on your machine. user: <%= ENV['USER'] %>
14
+ # Unable to automatically add the public key
15
+ <% end -%>
16
+
17
+ sudo yum install -y postgresql
18
+
19
+ # https://gist.github.com/juno/1330165
20
+ # Install developer tools
21
+ yum install -y git gcc make readline-devel openssl-devel
22
+
23
+ # Install ruby-build system-widely
24
+ git clone git://github.com/sstephenson/ruby-build.git /tmp/ruby-build
25
+ cd /tmp/ruby-build
26
+ ./install.sh
27
+ echo 'export PATH="/usr/local/bin:$PATH' >> ~/.bashrc
28
+
29
+ # Install rbenv for root
30
+ git clone git://github.com/sstephenson/rbenv.git ~/.rbenv
31
+ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
32
+ echo 'eval "$(rbenv init -)"' >> ~/.bashrc
33
+ set +u
34
+ source ~/.bashrc
35
+ set -u
36
+
37
+ # Install and enable ruby
38
+ rbenv install 2.5.0
39
+
40
+ # Install ruby for ec2-user also
41
+ cp -R ~/.rbenv /home/ec2-user/
42
+ chown -R ec2-user:ec2-user /home/ec2-user/.rbenv
43
+ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> /home/ec2-user/.bashrc
44
+ echo 'eval "$(rbenv init -)"' >> /home/ec2-user/.bashrc
45
+ echo '2.5.0' > /home/ec2-user/.ruby-version
46
+
47
+ uptime | tee /var/log/boot-time.log
data/exe/aws-ec2 ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Trap ^C
4
+ Signal.trap("INT") {
5
+ puts "\nCtrl-C detected. Exiting..."
6
+ sleep 1
7
+ exit
8
+ }
9
+
10
+ $:.unshift(File.expand_path("../../lib", __FILE__))
11
+ require "aws-ec2"
12
+ require "aws_ec2/cli"
13
+
14
+ AwsEc2::CLI.start(ARGV)
data/lib/aws-ec2.rb ADDED
@@ -0,0 +1,15 @@
1
+ $:.unshift(File.expand_path("../", __FILE__))
2
+ require "aws_ec2/version"
3
+ require "colorize"
4
+
5
+ module AwsEc2
6
+ autoload :Help, "aws_ec2/help"
7
+ autoload :Command, "aws_ec2/command"
8
+ autoload :CLI, "aws_ec2/cli"
9
+ autoload :AwsServices, "aws_ec2/aws_services"
10
+ autoload :Util, "aws_ec2/util"
11
+ autoload :Create, "aws_ec2/create"
12
+ autoload :Spot, "aws_ec2/spot"
13
+ autoload :TemplateHelper, "aws_ec2/template_helper"
14
+ autoload :UserData, "aws_ec2/user_data"
15
+ end
@@ -0,0 +1,7 @@
1
+ require 'aws-sdk-ec2'
2
+
3
+ module AwsEc2::AwsServices
4
+ def ec2
5
+ @ec2 ||= Aws::EC2::Client.new
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ module AwsEc2
2
+ class CLI < Command
3
+ class_option :verbose, type: :boolean
4
+ class_option :noop, type: :boolean
5
+ class_option :profile, desc: "profile name to use"
6
+
7
+ desc "create NAME", "create ec2 instance"
8
+ long_desc Help.text(:create)
9
+ def create(name)
10
+ Create.new(options.merge(name: name)).run
11
+ end
12
+
13
+ desc "spot NAME", "create spot ec2 instance"
14
+ long_desc Help.text(:spot)
15
+ def spot(name)
16
+ Spot.new(options.merge(name: name)).run
17
+ end
18
+
19
+ desc "userdata NAME", "displays generated userdata script"
20
+ long_desc Help.text(:user_data)
21
+ def userdata(name)
22
+ UserData.new(options.merge(name: name)).run
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ require "thor"
2
+
3
+ module AwsEc2
4
+ class Command < Thor
5
+ class << self
6
+ def dispatch(m, args, options, config)
7
+ # Allow calling for help via:
8
+ # aws_ec2 command help
9
+ # aws_ec2 command -h
10
+ # aws_ec2 command --help
11
+ # aws_ec2 command -D
12
+ #
13
+ # as well thor's normal way:
14
+ #
15
+ # aws_ec2 help command
16
+ help_flags = Thor::HELP_MAPPINGS + ["help"]
17
+ if args.length > 1 && !(args & help_flags).empty?
18
+ args -= help_flags
19
+ args.insert(-2, "help")
20
+ end
21
+ super
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,141 @@
1
+ require 'yaml'
2
+ require 'active_support/core_ext/hash'
3
+
4
+ module AwsEc2
5
+ class Create
6
+ include AwsServices
7
+ include Util
8
+
9
+ def initialize(options)
10
+ @options = options
11
+ end
12
+
13
+ def run
14
+ puts "Creating EC2 instance #{@options[:name]}..."
15
+ display_info
16
+ if @options[:noop]
17
+ puts "NOOP mode enabled. EC2 instance not created."
18
+ return
19
+ end
20
+
21
+ resp = ec2.run_instances(params)
22
+ puts "EC2 instance #{@options[:name]} created! 🎉"
23
+ puts "Visit https://console.aws.amazon.com/ec2/home to check on the status"
24
+ end
25
+
26
+ # params are main derived from profile files
27
+ def params
28
+ params = load_profiles(profile_name)
29
+ decorate_params(params)
30
+ normalize_launch_template(params).deep_symbolize_keys
31
+ end
32
+
33
+ def decorate_params(params)
34
+ upsert_name_tag(params)
35
+ params
36
+ end
37
+
38
+ # Adds instance ec2 tag if not already provided
39
+ def upsert_name_tag(params)
40
+ specs = params["tag_specifications"] || []
41
+
42
+ # insert an empty spec placeholder if one not found
43
+ spec = specs.find do |s|
44
+ s["resource_type"] == "instance"
45
+ end
46
+ unless spec
47
+ spec = {
48
+ "resource_type" => "instance",
49
+ "tags" => []
50
+ }
51
+ specs << spec
52
+ end
53
+ # guaranteed there's a tag_specifications with resource_type instance at this point
54
+
55
+ tags = spec["tags"] || []
56
+
57
+ unless tags.map { |t| t["key"] }.include?("Name")
58
+ tags << { "key" => "Name", "value" => @options[:name] }
59
+ end
60
+
61
+ specs = specs.map do |s|
62
+ # replace the name tag value
63
+ if s["resource_type"] == "instance"
64
+ {
65
+ "resource_type" => "instance",
66
+ "tags" => tags
67
+ }
68
+ else
69
+ s
70
+ end
71
+ end
72
+
73
+ params["tag_specifications"] = specs
74
+ params
75
+ end
76
+
77
+ # Allow adding launch template as a simple string.
78
+ #
79
+ # Standard structure:
80
+ # {
81
+ # launch_template: { launch_template_name: "TestLaunchTemplate" },
82
+ # }
83
+ #
84
+ # Simple string:
85
+ # {
86
+ # launch_template: "TestLaunchTemplate",
87
+ # }
88
+ #
89
+ # When launch_template is a simple String it will get transformed to the
90
+ # standard structure.
91
+ def normalize_launch_template(params)
92
+ if params["launch_template"].is_a?(String)
93
+ launch_template_identifier = params["launch_template"]
94
+ launch_template = if launch_template_identifier =~ /^lt-/
95
+ { "launch_template_id" => launch_template_identifier }
96
+ else
97
+ { "launch_template_name" => launch_template_identifier }
98
+ end
99
+ params["launch_template"] = launch_template
100
+ end
101
+ params
102
+ end
103
+
104
+ # Hard coded sensible defaults.
105
+ # Can be overridden easily with profiles
106
+ def defaults
107
+ {
108
+ max_count: 1,
109
+ min_count: 1,
110
+ }
111
+ end
112
+
113
+ def display_info
114
+ puts "Using the following parameters:"
115
+ pretty_display(params)
116
+
117
+ display_launch_template
118
+ end
119
+
120
+ def display_launch_template
121
+ launch_template = params[:launch_template]
122
+ return unless launch_template
123
+
124
+ resp = ec2.describe_launch_template_versions(
125
+ launch_template_id: launch_template[:launch_template_id],
126
+ launch_template_name: launch_template[:launch_template_name],
127
+ )
128
+ versions = resp.launch_template_versions
129
+ launch_template_data = {} # combined launch_template_data
130
+ versions.sort_by { |v| v[:version_number] }.each do |v|
131
+ launch_template_data.merge!(v[:launch_template_data])
132
+ end
133
+ puts "launch template data (versions combined):"
134
+ pretty_display(launch_template_data)
135
+ rescue Aws::EC2::Errors::InvalidLaunchTemplateNameNotFoundException => e
136
+ puts "ERROR: The specified launched template #{launch_template.inspect} was not found."
137
+ puts "Please double check that it exists."
138
+ exit
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,9 @@
1
+ module AwsEc2::Help
2
+ class << self
3
+ def text(namespaced_command)
4
+ path = namespaced_command.to_s.gsub(':','/')
5
+ path = File.expand_path("../help/#{path}.md", __FILE__)
6
+ IO.read(path) if File.exist?(path)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ Examples:
2
+
3
+ $ aws-ec2 create my-instance
@@ -0,0 +1,3 @@
1
+ Examples:
2
+
3
+ $ aws-ec2 spot my-request
@@ -0,0 +1,5 @@
1
+ Displays the generated user data script. Useful for debugging since ERB can be ran on the user-data scripts.
2
+
3
+ Given a user data script in profiles/user-data/myscript.sh, run:
4
+
5
+ $ aws-ec2 userdata myscript
@@ -0,0 +1,81 @@
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
@@ -0,0 +1,50 @@
1
+ require "base64"
2
+ require "erb"
3
+
4
+ module AwsEc2
5
+ module TemplateHelper
6
+ def user_data(name)
7
+ # allow user to specify the path also
8
+ if File.exist?(name)
9
+ name = File.basename(name) # normalize name, change path to name
10
+ end
11
+ name = File.basename(name, '.sh')
12
+ path = "#{root}/profiles/user-data/#{name}.sh"
13
+ result = erb_result(path)
14
+
15
+ Base64.encode64(result).strip
16
+ end
17
+
18
+ def erb_result(path)
19
+ template = IO.read(path)
20
+ begin
21
+ ERB.new(template, nil, "-").result(binding)
22
+ rescue Exception => e
23
+ puts e
24
+
25
+ # how to know where ERB stopped? - https://www.ruby-forum.com/topic/182051
26
+ # syntax errors have the (erb):xxx info in e.message
27
+ # undefined variables have (erb):xxx info in e.backtrac
28
+ error_info = e.message.split("\n").grep(/\(erb\)/)[0]
29
+ error_info ||= e.backtrace.grep(/\(erb\)/)[0]
30
+ raise unless error_info # unable to find the (erb):xxx: error line
31
+ line = error_info.split(':')[1].to_i
32
+ puts "Error evaluating ERB template on line #{line.to_s.colorize(:red)} of: #{path.sub(/^\.\//, '')}"
33
+
34
+ template_lines = template.split("\n")
35
+ context = 5 # lines of context
36
+ top, bottom = [line-context-1, 0].max, line+context-1
37
+ spacing = template_lines.size.to_s.size
38
+ template_lines[top..bottom].each_with_index do |line_content, index|
39
+ line_number = top+index+1
40
+ if line_number == line
41
+ printf("%#{spacing}d %s\n".colorize(:red), line_number, line_content)
42
+ else
43
+ printf("%#{spacing}d %s\n", line_number, line_content)
44
+ end
45
+ end
46
+ exit 1 unless ENV['TEST']
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,23 @@
1
+ require 'yaml'
2
+ require 'active_support/core_ext/hash'
3
+
4
+ module AwsEc2
5
+ class UserData
6
+ include TemplateHelper
7
+ include Util
8
+
9
+ def initialize(options)
10
+ @options = options
11
+ end
12
+
13
+ def run
14
+ if File.exist?(@options[:name])
15
+ filename = File.basename(@options[:name], '.sh')
16
+ end
17
+
18
+ filename ||= @options[:name]
19
+ path = "profiles/user-data/#{filename}.sh"
20
+ puts erb_result(path)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,63 @@
1
+ module AwsEc2
2
+ module Util
3
+ include TemplateHelper
4
+
5
+ def pretty_display(data)
6
+ data = data.deep_stringify_keys
7
+
8
+ message = "base64-encoded: use aws-ec2 userdata command to view"
9
+ # TODO: generalize this
10
+ data["user_data"] = message if data["user_data"]
11
+ data["spot_fleet_request_config"]["launch_specifications"].each do |spec|
12
+ spec["user_data"] = message if spec["user_data"]
13
+ end if data["spot_fleet_request_config"] && data["spot_fleet_request_config"]["launch_specifications"]
14
+
15
+ puts YAML.dump(data)
16
+ end
17
+
18
+ def load_profiles(profile_name)
19
+ return @profile_params if @profile_params
20
+
21
+ profile_file = "#{root}/profiles/#{profile_name}.yml"
22
+ base_path = File.dirname(profile_file)
23
+ default_file = "#{base_path}/default.yml"
24
+
25
+ params_exit_check!(profile_file, default_file)
26
+
27
+ defaults = load_profile(default_file)
28
+ params = load_profile(profile_file)
29
+ params = defaults.deep_merge(params)
30
+ @profile_params = params
31
+ end
32
+
33
+ def params_exit_check!(profile_file, default_file)
34
+ return if File.exist?(profile_file) or File.exist?(default_file)
35
+
36
+ puts "Unable to find a #{profile_file} or #{default_file} profile file."
37
+ puts "Please double check."
38
+ exit # EXIT HERE
39
+ end
40
+
41
+ def load_profile(file)
42
+ return {} unless File.exist?(file)
43
+
44
+ puts "Using profile: #{file}"
45
+ data = YAML.load(erb_result(file))
46
+ data ? data : {} # in case the file is empty
47
+ end
48
+
49
+ def profile_name
50
+ # allow user to specify the path also
51
+ if @options[:profile] && File.exist?(@options[:profile])
52
+ profile = File.basename(@options[:profile], '.yml')
53
+ end
54
+
55
+ # conventional profile is the name of the ec2 instance
56
+ profile || @options[:profile] || @options[:name]
57
+ end
58
+
59
+ def root
60
+ ENV['AWS_EC2_ROOT'] || '.'
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,3 @@
1
+ module AwsEc2
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,19 @@
1
+ require "spec_helper"
2
+
3
+ # to run specs with what"s remembered from vcr
4
+ # $ rake
5
+ #
6
+ # to run specs with new fresh data from aws api calls
7
+ # $ rake clean:vcr ; time rake
8
+ describe AwsEc2::CLI do
9
+ before(:all) do
10
+ @args = "--from Tung"
11
+ end
12
+
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")
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ ENV["TEST"] = "1"
2
+
3
+ # require "simplecov"
4
+ # SimpleCov.start
5
+
6
+ require "pp"
7
+
8
+ root = File.expand_path("../../", __FILE__)
9
+ require "#{root}/lib/aws-ec2"
10
+
11
+ module Helpers
12
+ def execute(cmd)
13
+ puts "Running: #{cmd}" if ENV["DEBUG"]
14
+ out = `#{cmd}`
15
+ puts out if ENV["DEBUG"]
16
+ out
17
+ end
18
+ end
19
+
20
+ RSpec.configure do |c|
21
+ c.include Helpers
22
+ end
metadata ADDED
@@ -0,0 +1,206 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aws-ec2
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tung Nguyen
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-01-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: hashie
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: colorize
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
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: byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
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: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: guard
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: guard-bundler
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: guard-rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: Simple tool to create AWS ec2 instances consistently with pre-configured
140
+ settings
141
+ email:
142
+ - tongueroo@gmail.com
143
+ executables:
144
+ - aws-ec2
145
+ extensions: []
146
+ extra_rdoc_files: []
147
+ files:
148
+ - ".gitignore"
149
+ - ".rspec"
150
+ - CHANGELOG.md
151
+ - Gemfile
152
+ - Gemfile.lock
153
+ - Guardfile
154
+ - LICENSE.txt
155
+ - README.md
156
+ - Rakefile
157
+ - aws-ec2.gemspec
158
+ - example/profiles/default.yml
159
+ - example/profiles/spot.yml
160
+ - example/profiles/spot/default.yml
161
+ - example/profiles/spot/dev.yml
162
+ - example/profiles/user-data/dev.sh
163
+ - exe/aws-ec2
164
+ - lib/aws-ec2.rb
165
+ - lib/aws_ec2/aws_services.rb
166
+ - lib/aws_ec2/cli.rb
167
+ - lib/aws_ec2/command.rb
168
+ - lib/aws_ec2/create.rb
169
+ - lib/aws_ec2/help.rb
170
+ - lib/aws_ec2/help/create.md
171
+ - lib/aws_ec2/help/spot.md
172
+ - lib/aws_ec2/help/user_data.md
173
+ - lib/aws_ec2/spot.rb
174
+ - lib/aws_ec2/template_helper.rb
175
+ - lib/aws_ec2/user_data.rb
176
+ - lib/aws_ec2/util.rb
177
+ - lib/aws_ec2/version.rb
178
+ - spec/lib/cli_spec.rb
179
+ - spec/spec_helper.rb
180
+ homepage: https://github.com/tongueroo/aws-ec2
181
+ licenses:
182
+ - MIT
183
+ metadata: {}
184
+ post_install_message:
185
+ rdoc_options: []
186
+ require_paths:
187
+ - lib
188
+ required_ruby_version: !ruby/object:Gem::Requirement
189
+ requirements:
190
+ - - ">="
191
+ - !ruby/object:Gem::Version
192
+ version: '0'
193
+ required_rubygems_version: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - ">="
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ requirements: []
199
+ rubyforge_project:
200
+ rubygems_version: 2.7.3
201
+ signing_key:
202
+ specification_version: 4
203
+ summary: Simple tool to create AWS ec2 instances
204
+ test_files:
205
+ - spec/lib/cli_spec.rb
206
+ - spec/spec_helper.rb