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