aws-ec2 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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