enhanced-elastic-beanstalk 1.2.3
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 +7 -0
- data/.DS_Store +0 -0
- data/.gitignore +23 -0
- data/.rspec +4 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +294 -0
- data/Rakefile +8 -0
- data/bin/elastic-beanstalk +25 -0
- data/enhanced-elastic-beanstalk.gemspec +48 -0
- data/lib/elastic/beanstalk.rb +15 -0
- data/lib/elastic/beanstalk/config.rb +139 -0
- data/lib/elastic/beanstalk/extensions.rb +45 -0
- data/lib/elastic/beanstalk/railtie.rb +17 -0
- data/lib/elastic/beanstalk/smoke_tester.rb +98 -0
- data/lib/elastic/beanstalk/spinner.rb +29 -0
- data/lib/elastic/beanstalk/tasks/eb.rake +486 -0
- data/lib/elastic/beanstalk/version.rb +5 -0
- data/spec/lib/elastic/beanstalk/eb_config_spec.rb +125 -0
- data/spec/lib/elastic/beanstalk/eb_extensions_spec.rb +28 -0
- data/spec/lib/elastic/beanstalk/eb_smoke_tester_spec.rb +13 -0
- data/spec/lib/elastic/beanstalk/eb_spec.yml +81 -0
- data/spec/spec_helper.rb +8 -0
- metadata +266 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
module Elastic
|
2
|
+
module Beanstalk
|
3
|
+
require 'elastic/beanstalk/railtie' if defined?(Rails::Railtie)
|
4
|
+
require 'elastic/beanstalk/config'
|
5
|
+
require 'elastic/beanstalk/extensions'
|
6
|
+
require 'elastic/beanstalk/smoke_tester'
|
7
|
+
require 'elastic/beanstalk/version'
|
8
|
+
require 'elastic/beanstalk/spinner'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
EbConfig = Elastic::Beanstalk::Config.instance
|
13
|
+
EbExtensions = Elastic::Beanstalk::Extensions
|
14
|
+
EbSmokeTester = Elastic::Beanstalk::SmokeTester
|
15
|
+
Spinner = Elastic::Beanstalk::Spinner
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'dry/config'
|
3
|
+
|
4
|
+
module Elastic
|
5
|
+
module Beanstalk
|
6
|
+
#
|
7
|
+
# EbConfig allows for default settings and mounting a specific environment with overriding
|
8
|
+
# hash values and merging of array values.
|
9
|
+
#
|
10
|
+
# NOTE: Anything can be overridden and merged into top-level settings (hashes) including
|
11
|
+
# anything that is an array value. Array values are merged *not* replaced. If you think
|
12
|
+
# something is screwy, see the defaults in the #init as those add some default array values.
|
13
|
+
# If this behavior of merging arrays or the defaults are somehow un-sensible, file an issue and we'll revisit it.
|
14
|
+
#
|
15
|
+
class Config < Dry::Config::Base
|
16
|
+
|
17
|
+
include Singleton
|
18
|
+
# it's a singleton, thus implemented as a self-extended module
|
19
|
+
# extend self
|
20
|
+
|
21
|
+
def initialize(options = {})
|
22
|
+
# seed the sensible defaults here
|
23
|
+
options = {
|
24
|
+
symbolize: true,
|
25
|
+
interpolation: false,
|
26
|
+
default_configuration: {
|
27
|
+
environment: nil,
|
28
|
+
secrets_dir: '~/.aws',
|
29
|
+
disallow_environments: %w(cucumber test),
|
30
|
+
strategy: :blue_green,
|
31
|
+
package: {
|
32
|
+
dir: 'pkg',
|
33
|
+
verbose: false,
|
34
|
+
includes: %w(**/* .ebextensions/**/*),
|
35
|
+
exclude_files: [],
|
36
|
+
exclude_dirs: %w(pkg tmp log test-reports)
|
37
|
+
},
|
38
|
+
options: {},
|
39
|
+
inactive: {}
|
40
|
+
}
|
41
|
+
}.merge(options)
|
42
|
+
super(options)
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def load!(environment = nil, filename = resolve_path('config/eb.yml'))
|
47
|
+
super(environment, filename)
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
def resolve_path(relative_path)
|
52
|
+
if defined?(Rails)
|
53
|
+
Rails.root.join(relative_path)
|
54
|
+
elsif defined?(Rake.original_dir)
|
55
|
+
File.expand_path(relative_path, Rake.original_dir)
|
56
|
+
else
|
57
|
+
File.expand_path(relative_path, Dir.pwd)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# custom methods for the specifics of eb.yml settings
|
62
|
+
def option_settings
|
63
|
+
generate_settings(options)
|
64
|
+
end
|
65
|
+
|
66
|
+
def inactive_settings
|
67
|
+
generate_settings(inactive)
|
68
|
+
end
|
69
|
+
|
70
|
+
def set_option(namespace, option_name, value)
|
71
|
+
current_options = to_option_setting(namespace, option_name, value)
|
72
|
+
namespace = current_options[:namespace].to_sym
|
73
|
+
option_name = current_options[:option_name].to_sym
|
74
|
+
|
75
|
+
options[namespace] = {} if options[namespace].nil?
|
76
|
+
options[namespace][option_name] = value
|
77
|
+
end
|
78
|
+
|
79
|
+
def find_option_setting(name)
|
80
|
+
find_setting(name, options)
|
81
|
+
end
|
82
|
+
|
83
|
+
def find_option_setting_value(name)
|
84
|
+
find_setting_value(name, options)
|
85
|
+
end
|
86
|
+
|
87
|
+
def find_inactive_setting(name)
|
88
|
+
find_setting(name, inactive)
|
89
|
+
end
|
90
|
+
|
91
|
+
def find_inactive_setting_value(name)
|
92
|
+
find_setting_value(name, inactive)
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_option_setting(namespace, option_name, value)
|
96
|
+
erb_value = "#{value}".scan(/<%=.*%>/).first
|
97
|
+
unless erb_value.nil?
|
98
|
+
value = ERB.new(erb_value).result
|
99
|
+
end
|
100
|
+
{
|
101
|
+
:'namespace' => "#{namespace}",
|
102
|
+
:'option_name' => "#{option_name}",
|
103
|
+
:'value' => "#{value}"
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def find_setting(name, settings_root)
|
110
|
+
name = name.to_sym
|
111
|
+
settings_root.each_key do |namespace|
|
112
|
+
settings_root[namespace].each do |option_name, value|
|
113
|
+
if option_name.eql? name
|
114
|
+
return to_option_setting(namespace, option_name, value)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
return nil
|
119
|
+
end
|
120
|
+
|
121
|
+
def find_setting_value(name, settings_root)
|
122
|
+
o = find_setting(name, settings_root)
|
123
|
+
o[:value] unless o.nil?
|
124
|
+
end
|
125
|
+
|
126
|
+
def generate_settings(settings_root)
|
127
|
+
result = []
|
128
|
+
settings_root.each_key do |namespace|
|
129
|
+
settings_root[namespace].each do |option_name, value|
|
130
|
+
result << to_option_setting(namespace, option_name, value)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
#{"option_settings" => result}
|
135
|
+
result
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Elastic
|
2
|
+
module Beanstalk
|
3
|
+
|
4
|
+
module Extensions
|
5
|
+
# it's a singleton, thus implemented as a self-extended module
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def write_extensions
|
9
|
+
|
10
|
+
ebextensions = EbConfig.ebextensions
|
11
|
+
return if ebextensions.nil?
|
12
|
+
|
13
|
+
Dir.mkdir absolute_file_name(nil) rescue nil
|
14
|
+
|
15
|
+
ebextensions.each_key do |filename|
|
16
|
+
contents = EbConfig.ebextensions[filename]
|
17
|
+
|
18
|
+
filename = absolute_file_name(filename)
|
19
|
+
|
20
|
+
# when converting to_yaml, kill the symbols as EB doesn't like it.
|
21
|
+
contents = contents.deep_symbolize(true).to_yaml.gsub(/---\n/, "")
|
22
|
+
#puts "\n#{filename}:\n----------------------------------------------------\n#{contents}----------------------------------------------------\n"
|
23
|
+
File.write(filename, contents)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete_extensions
|
28
|
+
ebextensions = EbConfig.ebextensions
|
29
|
+
return if ebextensions.nil?
|
30
|
+
|
31
|
+
ebextensions.each_key do |filename|
|
32
|
+
File.delete(absolute_file_name filename)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def absolute_file_name(filename)
|
37
|
+
EbConfig.resolve_path(".ebextensions/#{filename}")
|
38
|
+
end
|
39
|
+
|
40
|
+
def ebextensions_dir(filename)
|
41
|
+
EbConfig.resolve_path(".ebextensions/#{filename}")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
#require 'beanstalk'
|
2
|
+
#require 'rails'
|
3
|
+
|
4
|
+
module Elastic
|
5
|
+
module Beanstalk
|
6
|
+
|
7
|
+
# https://gist.github.com/josevalim/af7e572c2dc973add221
|
8
|
+
class Railtie < Rails::Railtie
|
9
|
+
|
10
|
+
#railtie_name :elastic
|
11
|
+
|
12
|
+
rake_tasks do
|
13
|
+
load 'elastic/beanstalk/tasks/eb.rake'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'net/http'
|
3
|
+
require 'elastic/beanstalk/spinner'
|
4
|
+
require 'nokogiri'
|
5
|
+
|
6
|
+
module Elastic
|
7
|
+
module Beanstalk
|
8
|
+
|
9
|
+
module SmokeTester
|
10
|
+
# it's a singleton, thus implemented as a self-extended module
|
11
|
+
extend self
|
12
|
+
|
13
|
+
def test_url(url, timeout, sleep_wait, expected_text)
|
14
|
+
|
15
|
+
puts '-------------------------------------------------------------------------------'
|
16
|
+
# puts "Smoke Testing: \n\turl: #{url}\n\ttimeout: #{timeout}\n\tsleep_wait: #{sleep_wait}\n\texpected_text: #{expected_text}\n"
|
17
|
+
puts "Smoke Test: \n\turl: #{url}\n\ttimeout: #{timeout}\n\texpected_text: #{expected_text}"
|
18
|
+
response = nil
|
19
|
+
begin
|
20
|
+
Timeout.timeout(timeout) do
|
21
|
+
i = 0
|
22
|
+
print "\nRunning..."
|
23
|
+
Spinner.show {
|
24
|
+
begin
|
25
|
+
sleep sleep_wait.to_i unless (i == 0)
|
26
|
+
i += 1
|
27
|
+
begin
|
28
|
+
response = Net::HTTP.get_response(URI(url))
|
29
|
+
#rescue SocketError => e
|
30
|
+
# response = ResponseStub.new({code: e.message, body: ''})
|
31
|
+
rescue => e
|
32
|
+
response = ResponseStub.new({code: e.message, body: ''})
|
33
|
+
end
|
34
|
+
|
35
|
+
puts "\t\t[#{response.code}]"
|
36
|
+
#puts "\t#{response.body}"
|
37
|
+
|
38
|
+
#
|
39
|
+
# Let's try to get out quickly when the deploy has gone wrong.
|
40
|
+
#
|
41
|
+
break if received_fatal_error?(response)
|
42
|
+
|
43
|
+
end until (!response.nil? && response.code.to_i == 200 && response.body.include?(expected_text))
|
44
|
+
}
|
45
|
+
end
|
46
|
+
ensure
|
47
|
+
puts "\n\nFinal response code: [#{response.code}] expectation met: #{response.body.include?(expected_text)}"
|
48
|
+
puts '-------------------------------------------------------------------------------'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def received_fatal_error?(response)
|
55
|
+
fatal_error = false
|
56
|
+
#['Bundler::PathError'].each do |code|
|
57
|
+
# fail right away on known terminal cases.
|
58
|
+
#if (!response.nil? && response.code.to_i == 500 && response.body.include?(code))
|
59
|
+
if (!response.nil? && response.code.to_i == 500 && response.body.include?('rror'))
|
60
|
+
|
61
|
+
doc = Nokogiri::HTML(response.body)
|
62
|
+
|
63
|
+
# By default, let's grab the info from the Passenger error page...
|
64
|
+
#$stderr.puts "\t\t[#{response.code}] #{code}"
|
65
|
+
$stderr.puts "\t\t[#{response.code}]"
|
66
|
+
$stderr.puts "\t\t\t#{get_content(doc, '//h1')}"
|
67
|
+
$stderr.puts "\t\t\t#{get_content(doc, '//dl/dd')}"
|
68
|
+
fatal_error = true
|
69
|
+
# break
|
70
|
+
end
|
71
|
+
#end
|
72
|
+
|
73
|
+
fatal_error
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_content(doc, xpath)
|
77
|
+
value = ''
|
78
|
+
begin
|
79
|
+
element = doc.xpath(xpath)
|
80
|
+
element = element.first unless element.nil?
|
81
|
+
value = element.content unless element.nil?
|
82
|
+
rescue
|
83
|
+
end
|
84
|
+
value
|
85
|
+
end
|
86
|
+
|
87
|
+
class ResponseStub
|
88
|
+
|
89
|
+
attr_reader :code, :body
|
90
|
+
|
91
|
+
def initialize(args)
|
92
|
+
@code = args[:code]
|
93
|
+
@body = args[:body]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Elastic
|
2
|
+
module Beanstalk
|
3
|
+
|
4
|
+
module Spinner
|
5
|
+
# it's a singleton, thus implemented as a self-extended module
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def show(fps=10)
|
9
|
+
chars = %w{ | / - \\ }
|
10
|
+
delay = 1.0/fps
|
11
|
+
iter = 0
|
12
|
+
spinner = Thread.new do
|
13
|
+
while iter do # Keep spinning until told otherwise
|
14
|
+
|
15
|
+
print chars[0]
|
16
|
+
sleep delay
|
17
|
+
print "\b"
|
18
|
+
chars.push chars.shift
|
19
|
+
end
|
20
|
+
end
|
21
|
+
yield.tap {# After yielding to the block, save the return value
|
22
|
+
iter = false # Tell the thread to exit, cleaning up after itself…
|
23
|
+
spinner.join # …and wait for it to do so.
|
24
|
+
} # Use the block's return value as the method's
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,486 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'zip'
|
3
|
+
require 'ap' # gem 'awesome_print'
|
4
|
+
require 'eb_deployer'
|
5
|
+
require 'time_diff'
|
6
|
+
require 'elastic/beanstalk'
|
7
|
+
require 'yaml'
|
8
|
+
require 'table_print'
|
9
|
+
require 'timeout'
|
10
|
+
require 'hipchat'
|
11
|
+
require 'pry'
|
12
|
+
|
13
|
+
namespace :eb do
|
14
|
+
|
15
|
+
namespace :rds do
|
16
|
+
# http://docs.aws.amazon.com/AWSRubySDK/latest/frames.html
|
17
|
+
desc 'List RDS snapshots'
|
18
|
+
task :snapshots => [:config] do |t, args|
|
19
|
+
# absolutely do not run this without specifying columns, otherwise it will call all defined methods including :delete
|
20
|
+
print_snapshots(rds.describe_db_snapshots.db_snapshots)
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'List RDS instances'
|
24
|
+
task :instances => [:config] do |t, args|
|
25
|
+
# absolutely do not run this without specifying columns, otherwise it will call all defined methods including :delete
|
26
|
+
|
27
|
+
print_instances(rds.describe_db_instances.db_instances)
|
28
|
+
end
|
29
|
+
|
30
|
+
desc 'Creates an RDS snapshot'
|
31
|
+
task :create_snapshot, [:instance_id, :snapshot_id] => [:config] do |t, args|
|
32
|
+
|
33
|
+
snapshot_id = args[:snapshot_id]
|
34
|
+
|
35
|
+
db = db(args[:instance_id])
|
36
|
+
|
37
|
+
from_time = Time.now
|
38
|
+
puts "\n\n---------------------------------------------------------------------------------------------------------------------------------------"
|
39
|
+
snapshot = db.create_snapshot(snapshot_id)
|
40
|
+
#snapshot = snapshot(snapshot_id) # for quick testing of code below
|
41
|
+
|
42
|
+
|
43
|
+
#ID | STATUS | GB | TYPE | ENGINE | ZONE | CREATED_AT | INSTANCE_CREATE_TIME
|
44
|
+
#------|----------|----|--------|--------------|------------|------------|------------------------
|
45
|
+
#pre-2 | creating | 10 | manual | mysql 5.6.12 | us-east-1d | | 2013-09-10 21:37:27
|
46
|
+
# available
|
47
|
+
print_snapshots(snapshot)
|
48
|
+
puts "\n"
|
49
|
+
|
50
|
+
timeout = 20 * 60
|
51
|
+
sleep_wait = 5
|
52
|
+
begin
|
53
|
+
Timeout.timeout(timeout) do
|
54
|
+
i = 0
|
55
|
+
|
56
|
+
print "\nCreating snapshot[#{snapshot_id}]..."
|
57
|
+
Spinner.show {
|
58
|
+
begin
|
59
|
+
sleep sleep_wait.to_i unless (i == 0)
|
60
|
+
i += 1
|
61
|
+
|
62
|
+
snapshot = snapshot(snapshot_id)
|
63
|
+
|
64
|
+
end until (snapshot.status.eql? 'available')
|
65
|
+
}
|
66
|
+
end
|
67
|
+
ensure
|
68
|
+
puts "\n\nSnapshot[#{snapshot_id}]: #{snapshot.status}. Finished in #{Time.diff(from_time, Time.now, '%N %S')[:diff]}.\n"
|
69
|
+
puts "---------------------------------------------------------------------------------------------------------------------------------------\n\n"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def snapshot(snapshot_id)
|
74
|
+
Aws::RDS::DBSnapshot.new(snapshot_id)
|
75
|
+
end
|
76
|
+
|
77
|
+
def db(instance_id)
|
78
|
+
db_instance = Aws::RDS::DBInstance.new(instance_id)
|
79
|
+
raise "DB Instance[#{instance_id}] does not exist." unless db_instance.exists?
|
80
|
+
db_instance
|
81
|
+
end
|
82
|
+
|
83
|
+
def rds
|
84
|
+
@rds ||= Aws::RDS::Client.new(Aws.config)
|
85
|
+
@rds
|
86
|
+
end
|
87
|
+
|
88
|
+
def print_snapshots(snapshots)
|
89
|
+
tp snapshots,
|
90
|
+
{snapshot_id: {display_method: :db_snapshot_identifier}},
|
91
|
+
:status,
|
92
|
+
{gb: {display_method: :allocated_storage}},
|
93
|
+
{type: {display_method: :snapshot_type}},
|
94
|
+
{engine: lambda { |i| "#{i.engine} #{i.engine_version}" }},
|
95
|
+
{zone: {display_method: :availability_zone}},
|
96
|
+
:snapshot_create_time,
|
97
|
+
:instance_create_time
|
98
|
+
end
|
99
|
+
|
100
|
+
def print_instances(instances)
|
101
|
+
tp instances,
|
102
|
+
{instance_id: {display_method: :db_instance_identifier}},
|
103
|
+
{name: {display_method: :db_name}},
|
104
|
+
{status: {display_method: :db_instance_status}},
|
105
|
+
{gb: {display_method: :allocated_storage}},
|
106
|
+
:iops,
|
107
|
+
{class: {display_method: :db_instance_class}},
|
108
|
+
{engine: lambda { |i| "#{i.engine} #{i.engine_version}" }},
|
109
|
+
{zone: {display_method: :availability_zone}},
|
110
|
+
:multi_az,
|
111
|
+
{endpoint: {display_method: lambda { |i| "#{i.endpoint.address}"}, :width => 120}},
|
112
|
+
{port: {display_method: lambda { |i| "#{i.endpoint.port}"}}},
|
113
|
+
#:latest_restorable_time,
|
114
|
+
#:auto_minor_version_upgrade,
|
115
|
+
#:read_replica_db_instance_identifiers,
|
116
|
+
#:read_replica_source_db_instance_identifier,
|
117
|
+
#:backup_retention_period,
|
118
|
+
#:master_username,
|
119
|
+
{created_at: {display_method: :instance_create_time}}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
###########################################
|
125
|
+
#
|
126
|
+
#
|
127
|
+
#
|
128
|
+
desc 'Setup AWS.config and merge/override environments into one resolved configuration'
|
129
|
+
task :config, [:environment, :version] do |t, args|
|
130
|
+
|
131
|
+
#-------------------------------------------------------------------------------
|
132
|
+
# Resolve arguments in a backwards compatibile way (see https://github.com/alienfast/elastic-beanstalk/issues/12)
|
133
|
+
# This allows both the use of RAILS_ENV or the :environment parameter
|
134
|
+
#
|
135
|
+
# Previously, we relied solely on the environment to be passed in as RAILS_ENV. It is _sometimes_ more convenient to allow it to be passed as the :environment parameter.
|
136
|
+
# Since :environment is first, and :version used to be first, check the :environment against a version number regex and adjust as necessary.
|
137
|
+
bc_arg_environment = args[:environment]
|
138
|
+
unless /^(?:(\d+)\.)?(?:(\d+)\.)?(\*|\d+)$/.match(bc_arg_environment).nil?
|
139
|
+
|
140
|
+
arg_version = args[:version]
|
141
|
+
raise "Found version[#{bc_arg_environment}] passed as :environment, but also found a value for :version[#{arg_version}]. Please adjust arguments to be [:environment, :version] or use RAILS_ENV with a single [:version] argument" unless arg_version.nil?
|
142
|
+
|
143
|
+
# version was passed as :environment, adjust it.
|
144
|
+
arg_version = bc_arg_environment
|
145
|
+
arg_environment = nil
|
146
|
+
|
147
|
+
# puts "NOTE: Manipulated :environment argument to :version[#{bc_arg_environment}]."
|
148
|
+
else
|
149
|
+
# normal resolution of argements
|
150
|
+
arg_environment = args[:environment]
|
151
|
+
arg_version = args[:version]
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
# set the default environment to be development if not otherwise resolved
|
156
|
+
environment = arg_environment || ENV['RAILS_ENV'] || 'development'
|
157
|
+
|
158
|
+
# load the configuration from same dir (for standalone CI purposes) or from the rails config dir if within the rails project
|
159
|
+
filename = EbConfig.resolve_path('eb.yml')
|
160
|
+
unless File.exists? filename
|
161
|
+
filename = EbConfig.resolve_path('config/eb.yml')
|
162
|
+
end
|
163
|
+
EbConfig.load!(environment, filename)
|
164
|
+
|
165
|
+
# Let's be explicit regardless of 'production' being the eb's default shall we? Set RACK_ENV and RAILS_ENV based on the given environment
|
166
|
+
EbConfig.set_option(:'aws:elasticbeanstalk:application:environment', 'RACK_ENV', "#{EbConfig.environment}")
|
167
|
+
EbConfig.set_option(:'aws:elasticbeanstalk:application:environment', 'RAILS_ENV', "#{EbConfig.environment}")
|
168
|
+
|
169
|
+
|
170
|
+
# let's load secret, non-repo ENV vars from .secrets/env_vars.yml
|
171
|
+
secret_env_file = EbConfig.resolve_path('.secrets/env_vars.yml')
|
172
|
+
if File.exists? secret_env_file
|
173
|
+
puts "Using secret env vars from .secrets/env_vars.yml..."
|
174
|
+
vars = YAML::load_file secret_env_file
|
175
|
+
if vars[EbConfig.environment]
|
176
|
+
vars[EbConfig.environment].each { |key, val|
|
177
|
+
EbConfig.set_option(:'aws:elasticbeanstalk:application:environment', key, val)
|
178
|
+
}
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
#-------------------------------------------------------------------------------
|
183
|
+
# resolve the version and set the APP_VERSION environment variable
|
184
|
+
|
185
|
+
# try to use from task argument first
|
186
|
+
version = arg_version
|
187
|
+
file = resolve_absolute_package_file
|
188
|
+
if version.nil? && File.exists?(file)
|
189
|
+
# otherwise use the MD5 hash of the package file
|
190
|
+
version = Digest::MD5.file(file).hexdigest
|
191
|
+
end
|
192
|
+
|
193
|
+
# set the var, depending on the sequence of calls, this may be nil
|
194
|
+
# (i.e. :show_config with no :version argument) so omit it until we have something worthwhile.
|
195
|
+
EbConfig.set_option(:'aws:elasticbeanstalk:application:environment', 'APP_VERSION', "#{version}") unless version.nil?
|
196
|
+
|
197
|
+
#-------------------------------------------------------------------------------
|
198
|
+
# configure aws credentials. Depending on the called task, this may not be necessary parent task should call #credentials! for validation.
|
199
|
+
Aws.config.update({
|
200
|
+
credentials: Aws::Credentials.new(credentials['access_key_id'], credentials['secret_access_key']),
|
201
|
+
}) unless credentials.nil?
|
202
|
+
|
203
|
+
#-------------------------------------------------------------------------------
|
204
|
+
# configure aws region if specified in the eb.yml
|
205
|
+
Aws.config.update({
|
206
|
+
region: EbConfig.region,
|
207
|
+
}) unless EbConfig.region.nil?
|
208
|
+
|
209
|
+
end
|
210
|
+
|
211
|
+
###########################################
|
212
|
+
#
|
213
|
+
#
|
214
|
+
#
|
215
|
+
desc 'Show resolved configuration without doing anything.'
|
216
|
+
task :show_config, [:environment, :version] => [:config] do |t, args|
|
217
|
+
|
218
|
+
puts "Working Directory: #{Rake.original_dir}"
|
219
|
+
print_config
|
220
|
+
end
|
221
|
+
|
222
|
+
###########################################
|
223
|
+
#
|
224
|
+
#
|
225
|
+
#
|
226
|
+
desc 'Remove any generated package.'
|
227
|
+
task :clobber do |t, args|
|
228
|
+
# kill the old package dir
|
229
|
+
rm_r EbConfig.package[:dir] rescue nil
|
230
|
+
#puts "Clobbered #{EbConfig.package[:dir]}."
|
231
|
+
end
|
232
|
+
|
233
|
+
###########################################
|
234
|
+
#
|
235
|
+
# Elastic Beanstalk seems to be finicky with a tar.gz. Using a zip, EB wants the files to be at the
|
236
|
+
# root of the archive, not under a top level folder. Include this package task to make
|
237
|
+
# sure we don't need to learn about this again through long deploy cycles!
|
238
|
+
#
|
239
|
+
desc 'Package zip source bundle for Elastic Beanstalk and generate external Rakefile. (optional) specify the :version arg to make it available to the elastic beanstalk app dynamically via the APP_VERSION environment varable'
|
240
|
+
task :package, [:environment, :version] => [:clobber, :config] do |t, args|
|
241
|
+
|
242
|
+
begin
|
243
|
+
# write .ebextensions
|
244
|
+
EbExtensions.write_extensions
|
245
|
+
|
246
|
+
# include all
|
247
|
+
files = FileList[EbConfig.package[:includes]]
|
248
|
+
|
249
|
+
# exclude files
|
250
|
+
EbConfig.package[:exclude_files].each do |file|
|
251
|
+
files.exclude(file)
|
252
|
+
end
|
253
|
+
|
254
|
+
EbConfig.package[:exclude_dirs].each do |dir|
|
255
|
+
files.exclude("#{dir}/**/*")
|
256
|
+
files.exclude("#{dir}")
|
257
|
+
end
|
258
|
+
|
259
|
+
# ensure dir exists
|
260
|
+
mkdir_p EbConfig.package[:dir] rescue nil
|
261
|
+
|
262
|
+
# zip it up
|
263
|
+
Zip::File.open(package_file, Zip::File::CREATE) do |archive|
|
264
|
+
|
265
|
+
puts "\nCreating archive (#{package_file}):" if package_verbose?
|
266
|
+
files.each do |f|
|
267
|
+
|
268
|
+
if File.directory?(f)
|
269
|
+
puts "\t#{f}" if package_verbose?
|
270
|
+
archive.add(f, f)
|
271
|
+
else
|
272
|
+
puts "\t\t#{f}" if package_verbose?
|
273
|
+
archive.add(f, f)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
|
279
|
+
# write Rakefile for external CI/CD package deployment
|
280
|
+
File.open(package_rakefile, "w+") do |f|
|
281
|
+
f.write("spec = Gem::Specification.find_by_name('elastic-beanstalk', '>= #{Elastic::Beanstalk::VERSION}')\n")
|
282
|
+
f.write("load \"\#{spec.gem_dir}/lib/elastic/beanstalk/tasks/eb.rake\"")
|
283
|
+
end
|
284
|
+
|
285
|
+
puts "\nFinished creating archive (#{package_file})."
|
286
|
+
ensure
|
287
|
+
EbExtensions.delete_extensions
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
|
292
|
+
###########################################
|
293
|
+
#
|
294
|
+
#
|
295
|
+
#
|
296
|
+
desc 'Deploy to Elastic Beanstalk'
|
297
|
+
task :deploy, [:environment, :version] => [:config] do |t, args|
|
298
|
+
|
299
|
+
# If called individually, this is not necessary, but if called in succession to eb:package, we may need to re-resolve an MD5 hashed name.
|
300
|
+
# Since we allow variable use of arguments, it is easiest just to quickly re-enable and re-run the eb:config task since all the resolution
|
301
|
+
# of values is contained there.
|
302
|
+
Rake::Task['eb:config'].reenable
|
303
|
+
Rake::Task['eb:config'].invoke(*args)
|
304
|
+
|
305
|
+
|
306
|
+
# Leave off the dependency of :package, we need to package this in the build phase and save
|
307
|
+
# the artifact on bamboo. The deploy plan will handle this separately.
|
308
|
+
from_time = Time.now
|
309
|
+
|
310
|
+
# ensure credentials
|
311
|
+
credentials!
|
312
|
+
|
313
|
+
package = resolve_absolute_package_file
|
314
|
+
|
315
|
+
# check package file
|
316
|
+
raise "Package file not found #{package} (also checked current dir). Be sure to run the :package task subsequent to any :deploy attempts." if !File.exists? package
|
317
|
+
|
318
|
+
# Don't deploy to test or cucumber (or whatever is specified by :disallow_environments)
|
319
|
+
raise "#{EbConfig.environment} is one of the #{EbConfig.disallow_environments} disallowed environments. Configure it by changing the :disallow_environments in the eb.yml" if EbConfig.disallow_environments.include? EbConfig.environment
|
320
|
+
|
321
|
+
print_config
|
322
|
+
|
323
|
+
# Avoid known problems
|
324
|
+
if EbConfig.find_option_setting_value('InstanceType').nil?
|
325
|
+
sleep 1 # let the puts from :config task finish first
|
326
|
+
raise "Failure to set an InstanceType is known to cause problems with deployments (i.e. .aws-eb-startup-version error). Please set InstanceType in the eb.yml with something like:\n #{{options: {:'aws:autoscaling:launchconfiguration' => {InstanceType: 't1.micro'}}}.to_yaml}\n"
|
327
|
+
end
|
328
|
+
|
329
|
+
options = {
|
330
|
+
application: EbConfig.app,
|
331
|
+
environment: EbConfig.eb_environment || EbConfig.environment,
|
332
|
+
version_label: find_option_app_version,
|
333
|
+
solution_stack_name: EbConfig.solution_stack_name,
|
334
|
+
option_settings: EbConfig.option_settings,
|
335
|
+
inactive_settings: EbConfig.inactive_settings,
|
336
|
+
strategy: EbConfig.strategy.to_sym,
|
337
|
+
package: package
|
338
|
+
}
|
339
|
+
|
340
|
+
options[:package_bucket] = EbConfig.package_bucket unless EbConfig.package_bucket.nil?
|
341
|
+
options[:keep_latest] = EbConfig.keep_latest unless EbConfig.keep_latest.nil?
|
342
|
+
options[:version_prefix] = EbConfig.version_prefix unless EbConfig.version_prefix.nil?
|
343
|
+
options[:tier] = EbConfig.tier unless EbConfig.tier.nil?
|
344
|
+
options[:cname_prefix] = EbConfig.custom_cname unless EbConfig.custom_cname.nil?
|
345
|
+
|
346
|
+
unless EbConfig.smoke_test.nil?
|
347
|
+
options[:smoke_test] = eval EbConfig.smoke_test
|
348
|
+
end
|
349
|
+
|
350
|
+
user = (`id -u -n` rescue '').chomp
|
351
|
+
user = 'Someone(?)' if user.strip.blank?
|
352
|
+
version = find_option_app_version[0,10] rescue '?'
|
353
|
+
send_notification "(beanstalk) #{user} started deploy of #{EbConfig[:app].upcase} (#{version}) to #{EbConfig.environment.to_s.upcase}...", { color: 'purple' }
|
354
|
+
begin
|
355
|
+
EbDeployer.deploy(options)
|
356
|
+
|
357
|
+
time = Time.diff(from_time, Time.now, '%N %S')[:diff]
|
358
|
+
success_emoji = %w[ success successful yey goldstar excellent awesome ].sample
|
359
|
+
send_notification "(#{success_emoji}) Deployment of #{EbConfig[:app].upcase} (#{version}) to #{EbConfig.environment.to_s.upcase} finished in #{time}.", { color: 'green' }
|
360
|
+
puts "\nDeployment finished in #{time}.\n"
|
361
|
+
rescue Exception => e
|
362
|
+
send_notification "(ohcrap) Deployment of #{EbConfig[:app].upcase} (#{version}) to #{EbConfig.environment.to_s.upcase} failed.", { color: 'red' }
|
363
|
+
puts e.message
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
###########################################
|
368
|
+
#
|
369
|
+
#
|
370
|
+
#
|
371
|
+
desc '** Warning: Destroy Elastic Beanstalk application and *all* environments.'
|
372
|
+
task :destroy, [:environment, :force] => [:config] do |t, args|
|
373
|
+
|
374
|
+
if args[:force].eql? 'y'
|
375
|
+
destroy()
|
376
|
+
else
|
377
|
+
puts "Are you sure you wish to destroy #{EbConfig.app}-#{EbConfig.environment}? (y/n)"
|
378
|
+
input = STDIN.gets.strip
|
379
|
+
if input == 'y'
|
380
|
+
destroy()
|
381
|
+
else
|
382
|
+
puts 'Destroy canceled.'
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
##########################################
|
388
|
+
private
|
389
|
+
|
390
|
+
def rails_or_rack_env
|
391
|
+
EbConfig
|
392
|
+
end
|
393
|
+
|
394
|
+
def send_notification(msg, opts={})
|
395
|
+
return false unless EbConfig[:notifications]
|
396
|
+
EbConfig[:notifications].each do |service, settings|
|
397
|
+
case service.to_s.downcase
|
398
|
+
when 'hipchat'
|
399
|
+
send_hipchat_notification(msg, (opts[:color] || 'yellow'), settings)
|
400
|
+
else puts "[!] Unknown notification service: #{service}"
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
def send_hipchat_notification(msg, color, settings, api_version: 'v2')
|
406
|
+
client = HipChat::Client.new(settings[:api_token], :api_version => api_version)
|
407
|
+
client[settings[:room]].send('', msg, color: color, message_format: 'text')
|
408
|
+
end
|
409
|
+
|
410
|
+
# Use the version if given, otherwise use the MD5 hash. Make available via the eb APP_VERSION environment variable
|
411
|
+
def find_option_app_version
|
412
|
+
|
413
|
+
# if already set by a dependency call to :config, get out early
|
414
|
+
version = EbConfig.find_option_setting_value('APP_VERSION')
|
415
|
+
return version unless version.nil?
|
416
|
+
end
|
417
|
+
|
418
|
+
def print_config
|
419
|
+
# display helpful for figuring out problems later in the deployment logs.
|
420
|
+
puts "\n----------------------------------------------------------------------------------"
|
421
|
+
puts 'Elastic Beanstalk configuration:'
|
422
|
+
puts "\taccess_key_id: #{credentials['access_key_id']}"
|
423
|
+
puts "\tenvironment: #{EbConfig.environment}"
|
424
|
+
puts "\tversion: #{find_option_app_version}"
|
425
|
+
|
426
|
+
# pretty print things that will be useful to see in the deploy logs and omit clutter that usually doesn't cause us problems.
|
427
|
+
h = EbConfig.configuration.dup
|
428
|
+
h.delete(:package)
|
429
|
+
h.delete(:disallow_environments)
|
430
|
+
puts Hash[h.sort].deep_symbolize(true).to_yaml.gsub(/---\n/, "\n").gsub(/\n/, "\n\t")
|
431
|
+
puts "----------------------------------------------------------------------------------\n"
|
432
|
+
end
|
433
|
+
|
434
|
+
def destroy
|
435
|
+
Rake::Task['eb:config'].invoke
|
436
|
+
EbDeployer.destroy(application: EbConfig.app, environment: EbConfig.environment)
|
437
|
+
end
|
438
|
+
|
439
|
+
# validate file exists
|
440
|
+
def credentials!
|
441
|
+
raise "\nFailed to load AWS secrets: #{aws_secrets_file}.\nFile contents should look like:\naccess_key_id: XXXX\nsecret_access_key: XXXX\n\n" unless File.exists?(aws_secrets_file)
|
442
|
+
credentials
|
443
|
+
|
444
|
+
['access_key_id', 'secret_access_key'].each do |key|
|
445
|
+
value = credentials[key]
|
446
|
+
raise "\nThe #{key} must be specified in the #{aws_secrets_file}.\n\n" if value.nil?
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
# load from a user directory i.e. ~/.aws/acme.yml
|
451
|
+
def credentials
|
452
|
+
# load secrets from the user home directory
|
453
|
+
@credentials = YAML::load_file(aws_secrets_file) if @credentials.nil?
|
454
|
+
@credentials
|
455
|
+
end
|
456
|
+
|
457
|
+
def package_verbose?
|
458
|
+
EbConfig.package[:verbose] || false
|
459
|
+
end
|
460
|
+
|
461
|
+
def resolve_absolute_package_file
|
462
|
+
|
463
|
+
# first see if it is in the current dir, i.e. CI environment where the generated rakefile and pkg is dropped in the same place
|
464
|
+
file = EbConfig.resolve_path(package_file_name)
|
465
|
+
return file if File.exists? file
|
466
|
+
|
467
|
+
file = EbConfig.resolve_path(package_file)
|
468
|
+
return file
|
469
|
+
end
|
470
|
+
|
471
|
+
def package_file
|
472
|
+
"#{EbConfig.package[:dir]}/#{package_file_name}"
|
473
|
+
end
|
474
|
+
|
475
|
+
def package_file_name
|
476
|
+
"#{EbConfig.app}.zip"
|
477
|
+
end
|
478
|
+
|
479
|
+
def package_rakefile
|
480
|
+
"#{EbConfig.package[:dir]}/Rakefile"
|
481
|
+
end
|
482
|
+
|
483
|
+
def aws_secrets_file
|
484
|
+
File.expand_path("#{EbConfig.secrets_dir}/#{EbConfig.app}.yml")
|
485
|
+
end
|
486
|
+
end
|