eb_deployer_updated 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +14 -0
- data/CHANGELOG.md +127 -0
- data/Gemfile +10 -0
- data/LICENSE +22 -0
- data/README.md +136 -0
- data/Rakefile +12 -0
- data/TODOS.md +11 -0
- data/bin/eb_deploy +13 -0
- data/eb_deployer.gemspec +22 -0
- data/lib/eb_deployer/application.rb +96 -0
- data/lib/eb_deployer/aws_driver/beanstalk.rb +158 -0
- data/lib/eb_deployer/aws_driver/cloud_formation_driver.rb +53 -0
- data/lib/eb_deployer/aws_driver/s3_driver.rb +35 -0
- data/lib/eb_deployer/aws_driver.rb +8 -0
- data/lib/eb_deployer/cf_event_source.rb +26 -0
- data/lib/eb_deployer/cloud_formation_provisioner.rb +120 -0
- data/lib/eb_deployer/component.rb +45 -0
- data/lib/eb_deployer/config_loader.rb +64 -0
- data/lib/eb_deployer/default_component.rb +32 -0
- data/lib/eb_deployer/default_config.rb +20 -0
- data/lib/eb_deployer/default_config.yml +159 -0
- data/lib/eb_deployer/deployment_strategy/blue_green.rb +79 -0
- data/lib/eb_deployer/deployment_strategy/blue_only.rb +45 -0
- data/lib/eb_deployer/deployment_strategy/inplace_update.rb +16 -0
- data/lib/eb_deployer/deployment_strategy.rb +20 -0
- data/lib/eb_deployer/eb_environment.rb +204 -0
- data/lib/eb_deployer/eb_event_source.rb +35 -0
- data/lib/eb_deployer/environment.rb +60 -0
- data/lib/eb_deployer/event_poller.rb +51 -0
- data/lib/eb_deployer/package.rb +39 -0
- data/lib/eb_deployer/resource_stacks.rb +20 -0
- data/lib/eb_deployer/smoke_test.rb +23 -0
- data/lib/eb_deployer/tasks.rb +45 -0
- data/lib/eb_deployer/throttling_handling.rb +17 -0
- data/lib/eb_deployer/utils.rb +33 -0
- data/lib/eb_deployer/version.rb +3 -0
- data/lib/eb_deployer/version_cleaner.rb +30 -0
- data/lib/eb_deployer.rb +339 -0
- data/lib/generators/eb_deployer/install/install_generator.rb +82 -0
- data/lib/generators/eb_deployer/install/templates/eb_deployer.rake +1 -0
- data/lib/generators/eb_deployer/install/templates/eb_deployer.yml.erb +181 -0
- data/lib/generators/eb_deployer/install/templates/ebextensions/01_postgres_packages.config +5 -0
- data/lib/generators/eb_deployer/install/templates/postgres_rds.json +88 -0
- data/test/aws_driver_stubs.rb +350 -0
- data/test/beanstalk_test.rb +23 -0
- data/test/blue_green_deploy_test.rb +114 -0
- data/test/blue_only_deploy_test.rb +78 -0
- data/test/cf_event_poller_test.rb +32 -0
- data/test/cloud_formation_provisioner_test.rb +47 -0
- data/test/config_loader_test.rb +205 -0
- data/test/deploy_test.rb +42 -0
- data/test/eb_environment_test.rb +120 -0
- data/test/eb_event_poller_test.rb +32 -0
- data/test/inplace_update_deploy_test.rb +110 -0
- data/test/multi_components_deploy_test.rb +164 -0
- data/test/rails_generators_test.rb +67 -0
- data/test/resources_deploy_test.rb +191 -0
- data/test/smoke_test_test.rb +23 -0
- data/test/template_deploy_test.rb +13 -0
- data/test/test_helper.rb +68 -0
- data/test/tier_setting_deploy_test.rb +24 -0
- data/test/versions_deploy_test.rb +120 -0
- metadata +194 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module EbDeployer
|
|
2
|
+
module AWSDriver
|
|
3
|
+
class CloudFormationDriver
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@client = Aws::CloudFormation::Client.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def stack_exists?(name)
|
|
10
|
+
describe_stack(name)
|
|
11
|
+
true
|
|
12
|
+
rescue Aws::CloudFormation::Errors::ValidationError
|
|
13
|
+
false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create_stack(name, template, opts)
|
|
17
|
+
@client.create_stack(opts.merge(:stack_name => name,
|
|
18
|
+
:template_body => template,
|
|
19
|
+
:parameters => convert_parameters(opts[:parameters])))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def update_stack(name, template, opts)
|
|
23
|
+
@client.update_stack(opts.merge(:stack_name => name,
|
|
24
|
+
:template_body => template,
|
|
25
|
+
:parameters => convert_parameters(opts[:parameters])))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def query_output(name, key)
|
|
29
|
+
output = describe_stack(name)[:outputs].find { |o| o[:output_key] == key }
|
|
30
|
+
output && output[:output_value]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fetch_events(name, options={})
|
|
34
|
+
response = @client.describe_stack_events(options.merge(:stack_name => name))
|
|
35
|
+
return response.stack_events, response.next_token
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def describe_stack(name)
|
|
41
|
+
@client.describe_stacks(:stack_name => name)[:stacks].first
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def log(msg)
|
|
45
|
+
puts "[#{Time.now.utc}][cloud_formation_driver] #{msg}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def convert_parameters(params)
|
|
49
|
+
params.map { |k, v| {:parameter_key => k, :parameter_value => v}}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module EbDeployer
|
|
2
|
+
module AWSDriver
|
|
3
|
+
class S3Driver
|
|
4
|
+
def create_bucket(bucket_name)
|
|
5
|
+
s3.create_bucket(:bucket => bucket_name)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def bucket_exists?(bucket_name)
|
|
9
|
+
s3.bucket(bucket_name).exists?
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def object_length(bucket_name, obj_name)
|
|
13
|
+
obj(bucket_name, obj_name).content_length rescue nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def upload_file(bucket_name, obj_name, file)
|
|
17
|
+
o = obj(bucket_name, obj_name)
|
|
18
|
+
o.upload_file(file)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
def s3
|
|
23
|
+
Aws::S3::Resource.new(client: Aws::S3::Client.new)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def obj(bucket_name, obj_name)
|
|
27
|
+
s3.bucket(bucket_name).object(obj_name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def buckets
|
|
31
|
+
s3.buckets
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module EbDeployer
|
|
2
|
+
class CfEventSource
|
|
3
|
+
def initialize(stack_name, cf_driver)
|
|
4
|
+
@stack_name = stack_name
|
|
5
|
+
@cf_driver = cf_driver
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def get_anchor
|
|
9
|
+
events, _ = @cf_driver.fetch_events(@stack_name)
|
|
10
|
+
events.first
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def fetch_events(from_anchor, &block)
|
|
14
|
+
events, next_token = @cf_driver.fetch_events(@stack_name)
|
|
15
|
+
should_continue = yield(events)
|
|
16
|
+
fetch_next(next_token, &block) if next_token && should_continue
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
def fetch_next(next_token, &block)
|
|
21
|
+
events, next_token = @cf_driver.fetch_events(@stack_name, :next_token => next_token)
|
|
22
|
+
should_continue = yield(events)
|
|
23
|
+
fetch_next(next_token, &block) if next_token && should_continue
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
module EbDeployer
|
|
2
|
+
class ResourceNotInReadyState < StandardError
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
class CloudFormationProvisioner
|
|
6
|
+
SUCCESS_STATS = ["CREATE_COMPLETE", "UPDATE_COMPLETE"]
|
|
7
|
+
FAILED_STATS = ["CREATE_FAILED", "UPDATE_FAILED", "UPDATE_ROLLBACK_COMPLETE"]
|
|
8
|
+
|
|
9
|
+
def initialize(stack_name, cf_driver)
|
|
10
|
+
@stack_name = stack_name
|
|
11
|
+
@cf_driver = cf_driver
|
|
12
|
+
@poller = EventPoller.new(CfEventSource.new(@stack_name, @cf_driver))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def provision(resources, tags)
|
|
16
|
+
resources = symbolize_keys(resources)
|
|
17
|
+
template = File.read(resources[:template])
|
|
18
|
+
capabilities = resources[:capabilities] || []
|
|
19
|
+
params = resources[:inputs] || resources[:parameters] || {}
|
|
20
|
+
policy = File.read(resources[:policy]) if resources[:policy]
|
|
21
|
+
override_policy = resources[:override_policy] || false
|
|
22
|
+
anchor = nil
|
|
23
|
+
begin
|
|
24
|
+
if stack_exists?
|
|
25
|
+
anchor = @poller.get_anchor
|
|
26
|
+
update_stack(template, params, capabilities, policy, override_policy, tags)
|
|
27
|
+
else
|
|
28
|
+
create_stack(template, params, capabilities, policy, tags)
|
|
29
|
+
end
|
|
30
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
|
31
|
+
if e.message =~ /No updates are to be performed/
|
|
32
|
+
log(e.message)
|
|
33
|
+
return
|
|
34
|
+
else
|
|
35
|
+
raise
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
wait_for_stack_op_terminate(anchor)
|
|
39
|
+
log("Resource stack provisioned successfully")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def transform_outputs(resources)
|
|
43
|
+
resources = symbolize_keys(resources)
|
|
44
|
+
outputs = resources[:outputs] || {}
|
|
45
|
+
transforms = resources[:transforms] || {}
|
|
46
|
+
transform_output_to_settings(convert_to_transforms(outputs).merge(transforms))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def output(key)
|
|
50
|
+
@cf_driver.query_output(@stack_name, key)
|
|
51
|
+
rescue Aws::CloudFormation::Errors::ValidationError
|
|
52
|
+
raise ResourceNotInReadyState.new("Resource stack not in ready state yet, perhaps you should provision it first?")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
#todo: remove duplication
|
|
58
|
+
def symbolize_keys(hash)
|
|
59
|
+
hash.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def convert_to_transforms(outputs)
|
|
64
|
+
outputs.inject({}) do |memo, (key, value)|
|
|
65
|
+
memo[key] = lambda { |output_value| value.merge('value' => output_value) }
|
|
66
|
+
memo
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def update_stack(template, params, capabilities, policy, override_policy, tags)
|
|
71
|
+
opts = {:capabilities => capabilities, :parameters => params, :tags => tags}
|
|
72
|
+
if (policy)
|
|
73
|
+
opts[:stack_policy_during_update_body] = policy if override_policy
|
|
74
|
+
log("Using temporary stack policy to apply resource stack updates") if override_policy
|
|
75
|
+
opts[:stack_policy_body] = policy unless override_policy
|
|
76
|
+
log("Applying new stack policy to existing resource stack") unless override_policy
|
|
77
|
+
end
|
|
78
|
+
@cf_driver.update_stack(@stack_name, template, opts)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def stack_exists?
|
|
82
|
+
@cf_driver.stack_exists?(@stack_name)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def create_stack(template, params, capabilities, policy, tags)
|
|
86
|
+
opts = {:disable_rollback => true, :capabilities => capabilities, :parameters => params, :tags => tags}
|
|
87
|
+
opts[:stack_policy_body] = policy if policy
|
|
88
|
+
log("Applying stack policy to new resource stack") if policy
|
|
89
|
+
@cf_driver.create_stack(@stack_name, template, opts)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def transform_output_to_settings(transforms)
|
|
93
|
+
(transforms || []).inject([]) do |settings, pair|
|
|
94
|
+
key, transform = pair
|
|
95
|
+
settings << transform.call(output(key))
|
|
96
|
+
settings
|
|
97
|
+
end.flatten
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def wait_for_stack_op_terminate(anchor)
|
|
101
|
+
@poller.poll(anchor) do |event|
|
|
102
|
+
log_event(event)
|
|
103
|
+
if FAILED_STATS.include?(event.resource_status)
|
|
104
|
+
raise "Resource stack update failed!"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
break if event.logical_resource_id == @stack_name && SUCCESS_STATS.include?(event.resource_status)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def log_event(event)
|
|
112
|
+
puts "[#{event.timestamp}][cloud_formation_provisioner] #{event.resource_type}(#{event.logical_resource_id}) #{event.resource_status} \"#{event.resource_status_reason}\""
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def log(msg)
|
|
117
|
+
puts "[#{Time.now.utc}][cloud_formation_provisioner] #{msg}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module EbDeployer
|
|
2
|
+
class Component
|
|
3
|
+
attr_reader :name
|
|
4
|
+
|
|
5
|
+
def initialize(name, env, options, eb_driver)
|
|
6
|
+
@name = name
|
|
7
|
+
@env = env
|
|
8
|
+
@eb_driver = eb_driver
|
|
9
|
+
@options = options.dup
|
|
10
|
+
@component_eb_settings = @options.delete(:option_settings) || []
|
|
11
|
+
@component_inactive_settings = @options.delete(:inactive_settings) || []
|
|
12
|
+
strategy_name = @options[:strategy] || @env.strategy_name
|
|
13
|
+
@strategy = DeploymentStrategy.create(self, strategy_name)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def cname_prefix
|
|
17
|
+
@options[:cname_prefix] || default_cname_prefix
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def deploy(version_label, eb_settings, inactive_settings=[])
|
|
21
|
+
@strategy.test_compatibility(create_options)
|
|
22
|
+
@strategy.deploy(version_label,
|
|
23
|
+
eb_settings + @component_eb_settings,
|
|
24
|
+
inactive_settings + @component_inactive_settings)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def new_eb_env(suffix=nil, cname_prefix_overriding=nil)
|
|
28
|
+
EbEnvironment.new(@env.app_name,
|
|
29
|
+
[@env.name, @name, suffix].compact.join('-'),
|
|
30
|
+
@eb_driver,
|
|
31
|
+
create_options.merge(:cname_prefix => cname_prefix_overriding || cname_prefix))
|
|
32
|
+
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def create_options
|
|
38
|
+
@env.creation_opts.merge(@options)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def default_cname_prefix
|
|
42
|
+
[@env.app_name, @env.name, @name].join('-')
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
require 'digest'
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module EbDeployer
|
|
6
|
+
class ConfigLoader
|
|
7
|
+
include Utils
|
|
8
|
+
|
|
9
|
+
class EvalBinding
|
|
10
|
+
attr_reader :environment, :package_digest
|
|
11
|
+
def initialize(package_digest, env)
|
|
12
|
+
@package_digest = package_digest
|
|
13
|
+
@environment = env
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def random_hash
|
|
17
|
+
SecureRandom.hex[0..9]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def load(options)
|
|
22
|
+
options = options.dup
|
|
23
|
+
package_digest = package_digest(options[:package])
|
|
24
|
+
config_file = options.delete(:config_file)
|
|
25
|
+
|
|
26
|
+
env = options[:environment]
|
|
27
|
+
config_settings = load_config_settings(config_file, package_digest, env)
|
|
28
|
+
|
|
29
|
+
app_name = config_settings[:application]
|
|
30
|
+
|
|
31
|
+
common_settings = symbolize_keys(config_settings[:common])
|
|
32
|
+
common_settings[:version_label] ||= package_digest
|
|
33
|
+
|
|
34
|
+
envs = config_settings[:environments]
|
|
35
|
+
raise "Environment #{env} is not defined in #{config_file}" unless envs.has_key?(env)
|
|
36
|
+
env_settings = symbolize_keys(envs[env] || {})
|
|
37
|
+
env_option_settings = env_settings.delete(:option_settings) || []
|
|
38
|
+
|
|
39
|
+
ret = options.merge(common_settings).merge(env_settings)
|
|
40
|
+
ret[:application] = app_name
|
|
41
|
+
ret[:option_settings] ||= []
|
|
42
|
+
ret[:option_settings] += env_option_settings
|
|
43
|
+
ret
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def load_config_settings(config_file, package_digest, env)
|
|
49
|
+
yaml = ERB.new(File.read(config_file)).
|
|
50
|
+
result(eval_binding(package_digest, env))
|
|
51
|
+
symbolize_keys(YAML.load(yaml))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def eval_binding(package_digest, env)
|
|
55
|
+
EvalBinding.new(package_digest, env).instance_eval { binding }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def package_digest(package)
|
|
59
|
+
return nil unless package
|
|
60
|
+
return package unless File.exist?(package)
|
|
61
|
+
Digest::MD5.file(package).hexdigest
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module EbDeployer
|
|
2
|
+
class DefaultComponent
|
|
3
|
+
def initialize(env, creation_opts, strategy_name, eb_driver)
|
|
4
|
+
@env = env
|
|
5
|
+
@eb_driver = eb_driver
|
|
6
|
+
@creation_opts = creation_opts
|
|
7
|
+
@strategy = DeploymentStrategy.create(self, strategy_name)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def cname_prefix
|
|
11
|
+
@creation_opts[:cname_prefix] || default_cname_prefix
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def deploy(version_label, eb_settings, inactive_settings=[])
|
|
15
|
+
@strategy.test_compatibility(@creation_opts)
|
|
16
|
+
@strategy.deploy(version_label, eb_settings, inactive_settings)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def new_eb_env(suffix=nil, cname_prefix_overriding=nil)
|
|
20
|
+
EbEnvironment.new(@env.app_name,
|
|
21
|
+
[@env.name, suffix].compact.join('-'),
|
|
22
|
+
@eb_driver,
|
|
23
|
+
@creation_opts.merge(:cname_prefix => cname_prefix_overriding || cname_prefix))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def default_cname_prefix
|
|
29
|
+
[@env.app_name, @env.name].join('-')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module EbDeployer
|
|
2
|
+
class DefaultConfig
|
|
3
|
+
attr_reader :app_name
|
|
4
|
+
|
|
5
|
+
def initialize(app_name)
|
|
6
|
+
@app_name = app_name.gsub('_', '-')
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def write_to(path)
|
|
10
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
11
|
+
File.open(path, 'w') { |f| f << ERB.new(File.read(config_template)).result(binding) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def config_template
|
|
17
|
+
File.expand_path("../default_config.yml", __FILE__)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# application name
|
|
2
|
+
application: <%= app_name %>
|
|
3
|
+
|
|
4
|
+
# common settings for all environments
|
|
5
|
+
common:
|
|
6
|
+
# Solution stack for elastic beanstalk, default is 64bit tomcat 7 for JAVA app
|
|
7
|
+
# solution_stack_name: 64bit Amazon Linux 2014.03 v1.0.3 running Tomcat 7 Java 7
|
|
8
|
+
|
|
9
|
+
# Tier name for environments. Current supported values are WebServer and Worker
|
|
10
|
+
# tier: WebServer
|
|
11
|
+
|
|
12
|
+
# AWS region to deploy. Default to us-east-1
|
|
13
|
+
# region: us-west-1
|
|
14
|
+
|
|
15
|
+
# There are three deployment strategies: 'blue-green', 'blue-only', or 'inplace-update'.
|
|
16
|
+
# Blue green deployments keep two elastic beanstalk environments and always deploy to
|
|
17
|
+
# inactive one, to achieve zero downtime.
|
|
18
|
+
# Blue only deployments do everything that the blue green deployments do except for the final
|
|
19
|
+
# inactive to active CNAME swap leaving the newly deployed application on the inactive
|
|
20
|
+
# "blue" instance.
|
|
21
|
+
# Inplace-update strategy will only keep one environment, and update the version inplace on
|
|
22
|
+
# deploy. Inplace-update will save resources but will suffer from downtime.
|
|
23
|
+
# (All old environments need be destroyed when you switching between strategies.)
|
|
24
|
+
# Default strategy is 'blue-green'.
|
|
25
|
+
# strategy: blue-green
|
|
26
|
+
|
|
27
|
+
# Name of s3 bucket where uploaded application packages will be stored.
|
|
28
|
+
# Note that the string ".packages" will be added as a suffix to your bucket.
|
|
29
|
+
# So, if "thoughtworks.simple" is passed as the bucket name, the actual s3 bucket
|
|
30
|
+
# name will be thoughtworks.simple.packages. Default to application name.
|
|
31
|
+
# package_bucket: my-s3-bucket
|
|
32
|
+
|
|
33
|
+
# If phoenix mode is turned on, it will terminate the old elastic
|
|
34
|
+
# beanstalk environment and recreate on deploy. For blue-green
|
|
35
|
+
# deployment it terminates the inactive environment first then
|
|
36
|
+
# recreate it. This is useful to avoid configuration drift and
|
|
37
|
+
# accumulating state on the ec2 instances. Default is off but we recommend
|
|
38
|
+
# it to be turned on for production environment.
|
|
39
|
+
# phoenix_mode: off
|
|
40
|
+
|
|
41
|
+
# The tags you would like to be associated with your resources.
|
|
42
|
+
# These tags will only be used when you first launch an environment. If you are using
|
|
43
|
+
# phoenix_mode set as true each time you deploy you will get a new environment and therefore
|
|
44
|
+
# any changes to your tags. If phoenix_mode is false then it will only use your tags on the
|
|
45
|
+
# initial deploy.
|
|
46
|
+
# tags:
|
|
47
|
+
# my_tag_key: my_tag_value
|
|
48
|
+
|
|
49
|
+
# Specifies the maximum number of versions to keep. Older versions are removed
|
|
50
|
+
# and deleted from the S3 source bucket as well. If specified as zero or not
|
|
51
|
+
# specified, all versions will be kept. If a version_prefix is given, only removes
|
|
52
|
+
# version starting with the prefix.
|
|
53
|
+
# keep_latest: 200
|
|
54
|
+
|
|
55
|
+
# Specifies a prefix to prepend to the version label.
|
|
56
|
+
# This can be useful if you want to use different binaries for different
|
|
57
|
+
# environments.
|
|
58
|
+
# version_prefix:
|
|
59
|
+
|
|
60
|
+
# Generating version label for package to be deployed. A readable version label will
|
|
61
|
+
# provide better traceablity of your deployment process.
|
|
62
|
+
# By default setting is:
|
|
63
|
+
# version_label: <%%= package_digest %>
|
|
64
|
+
# which means using MD5 digest of the package file. If you deploy using build
|
|
65
|
+
# pipeline tool such as GO, switching to pipline counter is highly suggested to
|
|
66
|
+
# increase the readability. Following example will read pipeline counter from environment
|
|
67
|
+
# variable with a fall back to digest for local deployment:
|
|
68
|
+
# version_label: <%%= ENV['GO_PIPELINE_COUNTER'] || package_digest %>
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Smoke test value should be a piece of ruby code with access to single variable
|
|
72
|
+
# "host_name" -- environment DNS name. Smoke test snippet will be evaluated at
|
|
73
|
+
# the end of the deployment for inplace-update deployment. For blue-green
|
|
74
|
+
# deployment it will run after inactive environment update is completed and before
|
|
75
|
+
# switching over.
|
|
76
|
+
# Defining a smoke test is highly recommended for serious usage. By default we use
|
|
77
|
+
# The simplest one that just be checking server landing page using curl, e.g.
|
|
78
|
+
smoke_test: |
|
|
79
|
+
curl_http_code = "curl -s -o /dev/null -w \"%{http_code}\" http://#{host_name}"
|
|
80
|
+
Timeout.timeout(600) do
|
|
81
|
+
until ['200', '301', '302'].include?(`#{curl_http_code}`.strip)
|
|
82
|
+
sleep 5
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# List of Elastic Beanstalk health states that will be considered healthy when deploying.
|
|
87
|
+
# You may want to override default 'Green' with that list, if for example you are using
|
|
88
|
+
# Elastic Beanstalk worker tier and you don't want eb_deployer to fail if you have messages
|
|
89
|
+
# in your SQS queue.
|
|
90
|
+
# By default eb_deployer only considers 'Green' as the healthy state.
|
|
91
|
+
# For a list of all Elastic Beanstalk health states refer to:
|
|
92
|
+
# http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/using-features.healthstatus.html#using-features.healthstatus.colors
|
|
93
|
+
# accepted_healthy_states:
|
|
94
|
+
# - Green
|
|
95
|
+
# - Yellow
|
|
96
|
+
|
|
97
|
+
# Elastic Beanstalk settings that will apply to the environments you are
|
|
98
|
+
# deploying.
|
|
99
|
+
# For all available options take a look at
|
|
100
|
+
# http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/command-options.html
|
|
101
|
+
option_settings:
|
|
102
|
+
# Following is an example of set EC2 ssh key name. This allow you ssh into the ec2
|
|
103
|
+
# instance. See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html
|
|
104
|
+
# - namespace: aws:autoscaling:launchconfiguration
|
|
105
|
+
# option_name: EC2KeyName
|
|
106
|
+
# value: <my-ec2-key-name>
|
|
107
|
+
|
|
108
|
+
# Following is an example which changes EC2 instance type
|
|
109
|
+
# from t1.micro (default) to m1.small. Instances with t1.micro type sometime
|
|
110
|
+
# are not very responsible, so m1.small is suggested for saving time.
|
|
111
|
+
# But if you care about the marginal cost difference you can comment this out to
|
|
112
|
+
# go back to t1.micro.
|
|
113
|
+
- namespace: aws:autoscaling:launchconfiguration
|
|
114
|
+
option_name: InstanceType
|
|
115
|
+
value: m1.small
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# If resources specified, eb_deployer will use the CloudFormation
|
|
119
|
+
# template you provide to create a default CloudFormation stack with
|
|
120
|
+
# name <application_name>-<env-name> for the environment current
|
|
121
|
+
# deploying. And Outputs of the CloudFormation can be mapped to Elastic Beanstalk
|
|
122
|
+
# options settings.
|
|
123
|
+
# keys:
|
|
124
|
+
# template => CloudFormation template file with JSON format
|
|
125
|
+
# policy => CloudFormation policy file with JSON format
|
|
126
|
+
# override_policy => (false) If override_policy is true and a policy file is provided then the policy will temporarily override any existing policy on the resource stack during this update,
|
|
127
|
+
# otherwise the provided policy will replace any existing policy on the resource stack
|
|
128
|
+
# inputs => A Hash, input values for the CloudFormation template
|
|
129
|
+
# outputs => A Hash with key map to your CloudFormation template outputs and value as elastic beanstalk settings namespace and option_name.
|
|
130
|
+
# capabilities => An array. You need set it to ['CAPABILITY_IAM'] if the
|
|
131
|
+
# template include IAM Instance Profile.
|
|
132
|
+
resources:
|
|
133
|
+
|
|
134
|
+
# For example creating a RDS instance for blue green deployment(check jruby-rails4
|
|
135
|
+
# sample project which has a working example):
|
|
136
|
+
# template: config/my_rds.json
|
|
137
|
+
# inputs:
|
|
138
|
+
# DBPassword: <%%= ENV["MYDBPASSWORD"] %>
|
|
139
|
+
# outputs:
|
|
140
|
+
# RDSPassSecurityGroup:
|
|
141
|
+
# namespace: aws:autoscaling:launchconfiguration
|
|
142
|
+
# option_name: SecurityGroups
|
|
143
|
+
# RDSDatabaseConfig:
|
|
144
|
+
# namespace: aws:elasticbeanstalk:application:environment
|
|
145
|
+
# option_name: databaseConfig
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# You can define environment here. Each environment can overriden any common settings
|
|
149
|
+
environments:
|
|
150
|
+
dev:
|
|
151
|
+
# example for overriding common settings
|
|
152
|
+
# strategy: inplace-update
|
|
153
|
+
production:
|
|
154
|
+
option_settings:
|
|
155
|
+
# example for overriding common option_settings: providing least redundancy
|
|
156
|
+
# in production environment.
|
|
157
|
+
# - namespace: aws:autoscaling:asg
|
|
158
|
+
# option_name: MinSize
|
|
159
|
+
# value: "2"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module EbDeployer
|
|
2
|
+
module DeploymentStrategy
|
|
3
|
+
class BlueGreen
|
|
4
|
+
def initialize(component)
|
|
5
|
+
@component = component
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def test_compatibility(env_create_options)
|
|
9
|
+
@create_opts = env_create_options
|
|
10
|
+
tier = env_create_options[:tier]
|
|
11
|
+
|
|
12
|
+
if tier && tier.downcase == 'worker'
|
|
13
|
+
raise "Blue green deployment is not supported for Worker tier"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def deploy(version_label, env_settings, inactive_settings=[])
|
|
18
|
+
|
|
19
|
+
if !ebenvs.any?(&method(:active_ebenv?))
|
|
20
|
+
ebenv('a', @component.cname_prefix).
|
|
21
|
+
deploy(version_label, env_settings)
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
active_ebenv = ebenvs.detect(&method(:active_ebenv?))
|
|
26
|
+
inactive_ebenv = ebenvs.reject(&method(:active_ebenv?)).first
|
|
27
|
+
|
|
28
|
+
inactive_ebenv.deploy(version_label, env_settings)
|
|
29
|
+
active_ebenv.swap_cname_with(inactive_ebenv)
|
|
30
|
+
|
|
31
|
+
blue_green_terminate_inactive = @create_opts[:blue_green_terminate_inactive]
|
|
32
|
+
blue_green_terminate_inactive_wait = @create_opts[:blue_green_terminate_inactive_wait]
|
|
33
|
+
blue_green_terminate_inactive_sleep = @create_opts[:blue_green_terminate_inactive_sleep]
|
|
34
|
+
|
|
35
|
+
if blue_green_terminate_inactive
|
|
36
|
+
active_ebenv.log("Waiting #{blue_green_terminate_inactive_wait}s before terminating environment...")
|
|
37
|
+
|
|
38
|
+
# Loop until timeout reached or environment becomes Red
|
|
39
|
+
count = 0
|
|
40
|
+
loop do
|
|
41
|
+
break if count >= blue_green_terminate_inactive_wait or inactive_ebenv.health_state != 'Green'
|
|
42
|
+
sleep blue_green_terminate_inactive_sleep
|
|
43
|
+
count += blue_green_terminate_inactive_sleep
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if inactive_ebenv.health_state == 'Green'
|
|
47
|
+
active_ebenv.log("Active environment healthy, terminating inactive (black) environment")
|
|
48
|
+
active_ebenv.terminate
|
|
49
|
+
else
|
|
50
|
+
active_ebenv.log("Active environment changed state to unhealthy. Existing (black) environment will not be terminated")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
unless inactive_settings.empty? || blue_green_terminate_inactive
|
|
56
|
+
active_ebenv.log("applying inactive settings...")
|
|
57
|
+
active_ebenv.apply_settings(inactive_settings)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
def active_ebenv?(ebenv)
|
|
63
|
+
ebenv.cname_prefix == @component.cname_prefix
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def ebenvs
|
|
67
|
+
[ebenv('a'), ebenv('b')]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def ebenv(suffix, cname_prefix=nil)
|
|
71
|
+
@component.new_eb_env(suffix, cname_prefix || inactive_cname_prefix)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def inactive_cname_prefix
|
|
75
|
+
"#{@component.cname_prefix}-inactive"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|