eb_deployer 0.0.1
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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/eb_deployer.gemspec +20 -0
- data/lib/eb_deployer/beanstalk.rb +82 -0
- data/lib/eb_deployer/cloud_formation_provisioner.rb +82 -0
- data/lib/eb_deployer/deployment_strategy.rb +74 -0
- data/lib/eb_deployer/environment.rb +82 -0
- data/lib/eb_deployer/event_poller.rb +41 -0
- data/lib/eb_deployer/package.rb +33 -0
- data/lib/eb_deployer/s3_driver.rb +33 -0
- data/lib/eb_deployer/version.rb +3 -0
- data/lib/eb_deployer.rb +54 -0
- data/test/aws_driver_stubs.rb +93 -0
- data/test/deploy_test.rb +147 -0
- metadata +98 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 tworker
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# EbDeployer
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'eb_deployer'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install eb_deployer
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/eb_deployer.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/eb_deployer/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["wpc", "betarelease"]
|
6
|
+
gem.email = ["alex.hal9000@gmail.com", "sudhindra.r.rao@gmail.com"]
|
7
|
+
gem.description = %q{Elastic Beanstalk Deployer with different deployment strategies.}
|
8
|
+
gem.summary = %q{Pick strategies like InplaceUpdate, Blue/Green.}
|
9
|
+
gem.homepage = "https://github.com/ThoughtWorksStudios/eb_deployer"
|
10
|
+
|
11
|
+
gem.add_runtime_dependency 'aws-sdk'
|
12
|
+
gem.add_development_dependency 'minitest'
|
13
|
+
|
14
|
+
gem.files = `git ls-files`.split($\)
|
15
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
16
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
17
|
+
gem.name = "eb_deployer"
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
gem.version = EbDeployer::VERSION
|
20
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module EbDeployer
|
2
|
+
class Beanstalk
|
3
|
+
attr_reader :client
|
4
|
+
def initialize(client=AWS::ElasticBeanstalk.new.client)
|
5
|
+
@client = client
|
6
|
+
end
|
7
|
+
|
8
|
+
def update_environment(app_name, env_name, version, settings)
|
9
|
+
env_id = convert_env_name_to_id(app_name, [env_name]).first
|
10
|
+
@client.update_environment(:environment_id => env_id,
|
11
|
+
:version_label => version,
|
12
|
+
:option_settings => settings)
|
13
|
+
end
|
14
|
+
|
15
|
+
def environment_exists?(app_name, env_name)
|
16
|
+
alive_envs(app_name, [env_name]).any?
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_environment(app_name, env_name, stack_name, cname_prefix, version, settings)
|
20
|
+
request = {:application_name => app_name,
|
21
|
+
:environment_name => env_name,
|
22
|
+
:solution_stack_name => stack_name,
|
23
|
+
:version_label => version,
|
24
|
+
:option_settings => settings }
|
25
|
+
request[:cname_prefix] = cname_prefix if cname_prefix
|
26
|
+
@client.create_environment(request)
|
27
|
+
end
|
28
|
+
|
29
|
+
def create_application_version(app_name, version_label, source_bundle)
|
30
|
+
@client.create_application_version(:application_name => app_name,
|
31
|
+
:source_bundle => source_bundle,
|
32
|
+
:version_label => version_label)
|
33
|
+
end
|
34
|
+
|
35
|
+
def application_version_labels
|
36
|
+
@client.describe_application_versions[:application_versions].map { |apv| apv[:version_label] }
|
37
|
+
end
|
38
|
+
|
39
|
+
def fetch_events(app_name, env_name, params, &block)
|
40
|
+
response = @client.describe_events(params.merge(:application_name => app_name,
|
41
|
+
:environment_name => env_name))
|
42
|
+
return [response[:events], response[:next_token]]
|
43
|
+
end
|
44
|
+
|
45
|
+
def environment_cname_prefix(app_name, env_name)
|
46
|
+
cname = environment_cname(app_name, env_name)
|
47
|
+
if cname =~ /^(.+)\.elasticbeanstalk\.com/
|
48
|
+
$1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def environment_cname(app_name, env_name)
|
53
|
+
env = alive_envs(app_name, [env_name]).first
|
54
|
+
env && env[:cname]
|
55
|
+
end
|
56
|
+
|
57
|
+
def environment_health_state(app_name, env_name)
|
58
|
+
env = alive_envs(app_name, [env_name]).first
|
59
|
+
env && env[:health]
|
60
|
+
end
|
61
|
+
|
62
|
+
def environment_swap_cname(app_name, env1, env2)
|
63
|
+
env1_id, env2_id = convert_env_name_to_id(app_name, [env1, env2])
|
64
|
+
@client.swap_environment_cnam_es(:source_environment_id => env1_id,
|
65
|
+
:destination_environment_id => env2_id)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def convert_env_name_to_id(app_name, env_names)
|
71
|
+
envs = alive_envs(app_name, env_names)
|
72
|
+
envs.map { |env| env[:environment_id] }
|
73
|
+
end
|
74
|
+
|
75
|
+
def alive_envs(app_name, env_names=[])
|
76
|
+
envs = @client.describe_environments(:application_name => app_name, :environment_names => env_names)[:environments]
|
77
|
+
|
78
|
+
envs.select {|e| e[:status] != 'Terminated' }
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module EbDeployer
|
2
|
+
class CloudFormationProvisioner
|
3
|
+
SUCCESS_STATS = [:create_complete, :update_complete, :update_rollback_complete]
|
4
|
+
FAILED_STATS = [:create_failed, :update_failed]
|
5
|
+
|
6
|
+
def initialize(stack_name)
|
7
|
+
@stack_name = stack_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def provision(resources)
|
11
|
+
params = extract_params
|
12
|
+
template = File.read(resources[:template])
|
13
|
+
transforms = resources[:transforms]
|
14
|
+
|
15
|
+
stack.exists? ? update_stack(template, params) : create_stack(template, params)
|
16
|
+
wait_for_stack_op_terminate
|
17
|
+
transform_output_to_settings(transforms)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def update_stack(template, params)
|
23
|
+
begin
|
24
|
+
stack.update(:template => template, :parameters => params)
|
25
|
+
rescue AWS::CloudFormation::Errors::ValidationError => e
|
26
|
+
if e.message =~ /No updates are to be performed/
|
27
|
+
log(e.message)
|
28
|
+
else
|
29
|
+
raise
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_stack(template, params)
|
35
|
+
cloud_formation.stacks.create(@stack_name, template, {
|
36
|
+
:disable_rollback => true,
|
37
|
+
:parameters => params
|
38
|
+
})
|
39
|
+
end
|
40
|
+
|
41
|
+
def transform_output_to_settings(transforms)
|
42
|
+
(transforms || []).inject([]) do |settings, pair|
|
43
|
+
key, transform = pair
|
44
|
+
settings << transform.call(output(key))
|
45
|
+
settings
|
46
|
+
end.flatten
|
47
|
+
end
|
48
|
+
|
49
|
+
def wait_for_stack_op_terminate
|
50
|
+
begin
|
51
|
+
sleep 15
|
52
|
+
stats = stack_status
|
53
|
+
raise "Resource stack update failed!" if FAILED_STATS.include?(stats)
|
54
|
+
log "current status: #{stack_status}"
|
55
|
+
end while !SUCCESS_STATS.include?(stats)
|
56
|
+
end
|
57
|
+
|
58
|
+
def output(key)
|
59
|
+
stack.outputs.find { |o| o.key == key }.try(:value)
|
60
|
+
end
|
61
|
+
|
62
|
+
def extract_params
|
63
|
+
Hash[ENV.map {|k, v| k =~ /^AWSRESOURCES_(.*)/ ? [$1, v] : nil }.compact]
|
64
|
+
end
|
65
|
+
|
66
|
+
def log(msg)
|
67
|
+
puts "[#{Time.now.utc}][resources-stack] #{msg}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def stack_status
|
71
|
+
stack.status.downcase.to_sym
|
72
|
+
end
|
73
|
+
|
74
|
+
def stack
|
75
|
+
cloud_formation.stacks[@stack_name]
|
76
|
+
end
|
77
|
+
|
78
|
+
def cloud_formation
|
79
|
+
AWS::CloudFormation.new
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module EbDeployer
|
2
|
+
module DeploymentStrategy
|
3
|
+
class InplaceUpdate
|
4
|
+
def initialize(app, env_name, eb_driver, env_creation_opts)
|
5
|
+
@app = app
|
6
|
+
@env_name = env_name
|
7
|
+
@eb_driver = eb_driver
|
8
|
+
@env_creation_opts = env_creation_opts
|
9
|
+
end
|
10
|
+
|
11
|
+
def deploy(version_label, env_settings)
|
12
|
+
Environment.new(@app, @env_name, @eb_driver, @env_creation_opts).
|
13
|
+
deploy(version_label, env_settings)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class BlueGreen
|
18
|
+
def initialize(app, env_name, eb_driver, env_creation_opts)
|
19
|
+
@app = app
|
20
|
+
@env_name = env_name
|
21
|
+
@eb_driver = eb_driver
|
22
|
+
@major_cname_prefix = env_creation_opts[:cname_prefix]
|
23
|
+
@solution_stack = env_creation_opts[:solution_stack]
|
24
|
+
@smoke_test = env_creation_opts[:smoke_test]
|
25
|
+
end
|
26
|
+
|
27
|
+
def deploy(version_label, env_settings)
|
28
|
+
if !envs.any?(&method(:active_env?))
|
29
|
+
env('blue', @major_cname_prefix).
|
30
|
+
deploy(version_label, env_settings)
|
31
|
+
return
|
32
|
+
end
|
33
|
+
|
34
|
+
active_env = envs.detect(&method(:active_env?))
|
35
|
+
inactive_env = envs.reject(&method(:active_env?)).first
|
36
|
+
|
37
|
+
inactive_env.deploy(version_label, env_settings)
|
38
|
+
active_env.swap_cname_with(inactive_env)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def active_env?(env)
|
43
|
+
env.cname_prefix == @major_cname_prefix
|
44
|
+
end
|
45
|
+
|
46
|
+
def envs
|
47
|
+
[env('blue'), env('green')]
|
48
|
+
end
|
49
|
+
|
50
|
+
def env(color, cname_prefix=nil)
|
51
|
+
Environment.new(@app, @env_name + '-' + color, @eb_driver,
|
52
|
+
:solution_stack => @solution_stack,
|
53
|
+
:cname_prefix => cname_prefix || inactive_cname_prefix,
|
54
|
+
:smoke_test => @smoke_test)
|
55
|
+
end
|
56
|
+
|
57
|
+
def inactive_cname_prefix
|
58
|
+
"#{@app}-#{@env_name}-inactive"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.create(strategy_name, app, env_name, eb_driver, env_creation_opts={})
|
63
|
+
case strategy_name.to_sym
|
64
|
+
when :inplace_update
|
65
|
+
InplaceUpdate.new(app, env_name, eb_driver, env_creation_opts)
|
66
|
+
when :blue_green
|
67
|
+
BlueGreen.new(app, env_name, eb_driver, env_creation_opts)
|
68
|
+
else
|
69
|
+
raise 'strategy_name:' + strategy_name + ' not supported'
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module EbDeployer
|
2
|
+
class Environment
|
3
|
+
attr_reader :app, :name
|
4
|
+
def initialize(app, env_name, eb_driver, creation_opts={})
|
5
|
+
@app = app
|
6
|
+
@name = env_name
|
7
|
+
@bs = eb_driver
|
8
|
+
@creation_opts = creation_opts
|
9
|
+
@poller = EventPoller.new(@app, @name, @bs)
|
10
|
+
end
|
11
|
+
|
12
|
+
def deploy(version_label, settings)
|
13
|
+
create_or_update_env(version_label, settings)
|
14
|
+
poll_events
|
15
|
+
smoke_test
|
16
|
+
wait_for_env_become_healthy
|
17
|
+
end
|
18
|
+
|
19
|
+
def cname_prefix
|
20
|
+
@bs.environment_cname_prefix(@app, @name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def ==(another)
|
24
|
+
self.app == another.app && self.name == another.name
|
25
|
+
end
|
26
|
+
|
27
|
+
def swap_cname_with(another)
|
28
|
+
@bs.environment_swap_cname(self.app, self.name, another.name)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def create_or_update_env(version_label, settings)
|
34
|
+
if @bs.environment_exists?(@app, @name)
|
35
|
+
@bs.update_environment(@app, @name, version_label, settings)
|
36
|
+
else
|
37
|
+
@bs.create_environment(@app, @name, @creation_opts[:solution_stack], @creation_opts[:cname_prefix], version_label, settings)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def smoke_test
|
42
|
+
if smoke = @creation_opts[:smoke_test]
|
43
|
+
host = @bs.environment_cname(@app, @name)
|
44
|
+
log("running smoke test for #{host}...")
|
45
|
+
smoke.call(host)
|
46
|
+
log("smoke test succeeded.")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def poll_events
|
51
|
+
@poller.poll do |event|
|
52
|
+
raise event[:message] if event[:message] =~ /Failed to deploy application/
|
53
|
+
|
54
|
+
log_event(event)
|
55
|
+
break if event[:message] =~ /Environment update completed successfully/ ||
|
56
|
+
event[:message] =~ /Successfully launched environment/
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def wait_for_env_become_healthy
|
61
|
+
Timeout.timeout(600) do
|
62
|
+
current_health_status = @bs.environment_health_state(@app, @name)
|
63
|
+
|
64
|
+
while current_health_status != 'Green'
|
65
|
+
log("health status: #{current_health_status}")
|
66
|
+
sleep 15
|
67
|
+
current_health_status = @bs.environment_health_state(@app, @name)
|
68
|
+
end
|
69
|
+
|
70
|
+
log("health status: #{current_health_status}")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def log(msg)
|
75
|
+
puts "[#{Time.now.utc}][beanstalk-#{@name}] #{msg}"
|
76
|
+
end
|
77
|
+
|
78
|
+
def log_event(event)
|
79
|
+
puts "[#{event[:event_date]}][beanstalk-#{@name}] #{event[:message]}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module EbDeployer
|
2
|
+
class EventPoller
|
3
|
+
def initialize(app, env, beanstalk)
|
4
|
+
@app, @env, @beanstalk, @start_time = app, env, beanstalk, Time.now
|
5
|
+
end
|
6
|
+
|
7
|
+
def poll(&block)
|
8
|
+
handled = Set.new
|
9
|
+
loop do
|
10
|
+
fetch_events do |events|
|
11
|
+
new_events = events.reject { |e| handled.include?(digest(e)) }
|
12
|
+
handle(new_events, &block)
|
13
|
+
handled += new_events.map { |e| digest(e) }
|
14
|
+
end
|
15
|
+
sleep 15
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def digest(event)
|
22
|
+
JSON.dump(event)
|
23
|
+
end
|
24
|
+
|
25
|
+
def handle(events, &block)
|
26
|
+
events.reverse.each(&block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def fetch_events(&block)
|
30
|
+
events, next_token = @beanstalk.fetch_events(@app, @env, :start_time => @start_time.iso8601)
|
31
|
+
yield(events)
|
32
|
+
fetch_next(next_token, &block) if next_token
|
33
|
+
end
|
34
|
+
|
35
|
+
def fetch_next(next_token, &block)
|
36
|
+
events, next_token = @beanstalk.fetch_events(@app, @env, :next_token => next_token)
|
37
|
+
yield(events)
|
38
|
+
fetch_next(next_token, &block) if next_token
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module EbDeployer
|
2
|
+
class Package
|
3
|
+
def initialize(file, bucket_name, s3_driver)
|
4
|
+
@file, @bucket_name = file, bucket_name
|
5
|
+
@s3 = s3_driver
|
6
|
+
end
|
7
|
+
|
8
|
+
def upload
|
9
|
+
ensure_bucket(@bucket_name)
|
10
|
+
upload_if_not_exists(@file, @bucket_name)
|
11
|
+
end
|
12
|
+
|
13
|
+
def source_bundle
|
14
|
+
{ :s3_bucket => @bucket_name, :s3_key => s3_path }
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def s3_path
|
20
|
+
@_s3_path ||= Digest::MD5.file(@file).hexdigest + "-" + File.basename(@file)
|
21
|
+
end
|
22
|
+
|
23
|
+
def ensure_bucket(bucket_name)
|
24
|
+
@s3.create_bucket(@bucket_name) unless @s3.bucket_exists?(@bucket_name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def upload_if_not_exists(file, bucket_name)
|
28
|
+
if @s3.object_length(@bucket_name, s3_path) != File.size(file)
|
29
|
+
@s3.upload_file(@bucket_name, s3_path, file)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module EbDeployer
|
2
|
+
class S3Driver
|
3
|
+
def create_bucket(bucket_name)
|
4
|
+
buckets.create(bucket_name)
|
5
|
+
end
|
6
|
+
|
7
|
+
def bucket_exists?(bucket_name)
|
8
|
+
buckets[bucket_name].exists?
|
9
|
+
end
|
10
|
+
|
11
|
+
def object_length(bucket_name, obj_name)
|
12
|
+
obj(bucket_name, obj_name).content_length rescue nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def upload_file(bucket_name, obj_name, file)
|
16
|
+
o = obj(bucket_name, obj_name)
|
17
|
+
File.open(file) { |f| o.write(f) }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def s3
|
22
|
+
AWS::S3.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def obj(bucket_name, obj_name)
|
26
|
+
buckets[bucket_name].objects[obj_name]
|
27
|
+
end
|
28
|
+
|
29
|
+
def buckets
|
30
|
+
s3.buckets
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/eb_deployer.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require "eb_deployer/version"
|
2
|
+
require "eb_deployer/deployment_strategy"
|
3
|
+
require "eb_deployer/beanstalk"
|
4
|
+
require "eb_deployer/cloud_formation_provisioner"
|
5
|
+
require "eb_deployer/environment"
|
6
|
+
require "eb_deployer/event_poller"
|
7
|
+
require "eb_deployer/package"
|
8
|
+
require 'eb_deployer/s3_driver'
|
9
|
+
require 'digest'
|
10
|
+
require 'set'
|
11
|
+
require 'time'
|
12
|
+
require 'json'
|
13
|
+
require 'timeout'
|
14
|
+
|
15
|
+
module EbDeployer
|
16
|
+
def self.deploy(opts)
|
17
|
+
# AWS.config(:logger => Logger.new($stdout))
|
18
|
+
if region = opts[:region]
|
19
|
+
AWS.config(:region => region)
|
20
|
+
end
|
21
|
+
|
22
|
+
bs = opts[:bs_driver] || Beanstalk.new
|
23
|
+
s3 = opts[:s3_driver] || S3Driver.new
|
24
|
+
stack_name = opts[:solution_stack_name] || "64bit Amazon Linux running Tomcat 7"
|
25
|
+
app = opts[:application]
|
26
|
+
env_name = opts[:environment]
|
27
|
+
version_label = opts[:version_label].to_s.strip
|
28
|
+
cname = opts[:cname]
|
29
|
+
env_settings = opts[:settings] || []
|
30
|
+
strategy_name = opts[:strategy] || :inplace_update
|
31
|
+
cname_prefix = opts[:cname_prefix] || [app, env_name].join('-')
|
32
|
+
smoke_test = opts[:smoke_test] || Proc.new {}
|
33
|
+
|
34
|
+
package = Package.new(opts[:package], app + "-packages", s3)
|
35
|
+
cf = CloudFormationProvisioner.new("#{app}-#{env_name}")
|
36
|
+
strategy = DeploymentStrategy.create(strategy_name, app, env_name, bs,
|
37
|
+
:solution_stack => stack_name,
|
38
|
+
:cname_prefix => cname_prefix,
|
39
|
+
:smoke_test => smoke_test)
|
40
|
+
|
41
|
+
if resources = opts[:resources]
|
42
|
+
env_settings += cf.provision(resources)
|
43
|
+
end
|
44
|
+
|
45
|
+
package.upload
|
46
|
+
|
47
|
+
unless bs.application_version_labels.include?(version_label)
|
48
|
+
bs.create_application_version(app, version_label, package.source_bundle)
|
49
|
+
end
|
50
|
+
|
51
|
+
strategy.deploy(version_label, env_settings)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
class EBStub
|
2
|
+
def initialize
|
3
|
+
@envs = {}
|
4
|
+
@versions = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def create_environment(app, env, solution_stack, cname_prefix, version, settings)
|
8
|
+
raise 'cname prefix is not avaible' if @envs.values.detect { |env| env[:cname_prefix] == cname_prefix }
|
9
|
+
@envs[env_key(app, env)] = {
|
10
|
+
:solution_stack => solution_stack,
|
11
|
+
:version => version,
|
12
|
+
:cname_prefix => cname_prefix,
|
13
|
+
:settings => settings}
|
14
|
+
end
|
15
|
+
|
16
|
+
def update_environment(app, env, version, settings)
|
17
|
+
@envs[env_key(app, env)].merge!(:version => version, :settings => settings)
|
18
|
+
end
|
19
|
+
|
20
|
+
def environment_exists?(app_name, env_name)
|
21
|
+
@envs.has_key?(env_key(app_name, env_name))
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_application_version(app_name, version_label, source_bundle)
|
25
|
+
@versions[app_name] ||= []
|
26
|
+
@versions[app_name] = { version_label => source_bundle }
|
27
|
+
end
|
28
|
+
|
29
|
+
def application_version_labels
|
30
|
+
@versions.values.map(&:keys).flatten
|
31
|
+
end
|
32
|
+
|
33
|
+
def fetch_events(app_name, env_name, options={})
|
34
|
+
[[{:event_date => Time.now.utc,
|
35
|
+
:message => 'Environment update completed successfully'}],
|
36
|
+
nil]
|
37
|
+
end
|
38
|
+
|
39
|
+
def environment_cname_prefix(app_name, env_name)
|
40
|
+
return unless @envs[env_key(app_name, env_name)]
|
41
|
+
@envs[env_key(app_name, env_name)][:cname_prefix] || app_name + "-" + SecureRandom.hex
|
42
|
+
end
|
43
|
+
|
44
|
+
def environment_cname(app_name, env_name)
|
45
|
+
return unless @envs[env_key(app_name, env_name)]
|
46
|
+
environment_cname_prefix(app_name, env_name) + ".elasticbeanstalk.com"
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def environment_swap_cname(app_name, env1_name, env2_name)
|
51
|
+
env1, env2 = @envs[env_key(app_name, env1_name)], @envs[env_key(app_name, env2_name)]
|
52
|
+
temp = env1[:cname_prefix]
|
53
|
+
env1[:cname_prefix] = env2[:cname_prefix]
|
54
|
+
env2[:cname_prefix] = temp
|
55
|
+
end
|
56
|
+
|
57
|
+
def environment_health_state(app_name, env_name)
|
58
|
+
'Green'
|
59
|
+
end
|
60
|
+
|
61
|
+
#test only
|
62
|
+
def environment_verion_label(app_name, env_name)
|
63
|
+
@envs[env_key(app_name, env_name)][:version]
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def env_key(app, name)
|
68
|
+
[app, name].join("-")
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
class S3Stub
|
74
|
+
def initialize
|
75
|
+
@buckets = {}
|
76
|
+
end
|
77
|
+
|
78
|
+
def create_bucket(bucket_name)
|
79
|
+
@buckets[bucket_name] = {}
|
80
|
+
end
|
81
|
+
|
82
|
+
def bucket_exists?(bucket_name)
|
83
|
+
@buckets.has_key?(bucket_name)
|
84
|
+
end
|
85
|
+
|
86
|
+
def object_length(bucket_name, obj_name)
|
87
|
+
@buckets[bucket_name][obj_name] && File.size(@buckets[bucket_name][obj_name])
|
88
|
+
end
|
89
|
+
|
90
|
+
def upload_file(bucket_name, obj_name, file)
|
91
|
+
@buckets[bucket_name][obj_name] = file
|
92
|
+
end
|
93
|
+
end
|
data/test/deploy_test.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'eb_deployer'
|
2
|
+
require 'aws_driver_stubs'
|
3
|
+
require 'minitest/autorun'
|
4
|
+
|
5
|
+
class DeployTest < Minitest::Test
|
6
|
+
def setup
|
7
|
+
@eb_driver = EBStub.new
|
8
|
+
@s3_driver = S3Stub.new
|
9
|
+
@sample_package = '/tmp/app-package.war'
|
10
|
+
File.open(@sample_package, 'w') { |f| f << 's' * 100 }
|
11
|
+
end
|
12
|
+
|
13
|
+
def deploy(opts)
|
14
|
+
EbDeployer.deploy({:package => @sample_package,
|
15
|
+
:bs_driver => @eb_driver,
|
16
|
+
:s3_driver => @s3_driver,
|
17
|
+
:version_label => 1}.merge(opts))
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def test_first_deployment_create_environment
|
22
|
+
assert !@eb_driver.environment_exists?('simple', 'production')
|
23
|
+
deploy(:application => 'simple', :environment => "production")
|
24
|
+
assert @eb_driver.environment_exists?('simple', 'production')
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def test_update_environment_with_new_version_should_change_version_that_deployed
|
29
|
+
deploy(:application => 'simple',
|
30
|
+
:environment => "production",
|
31
|
+
:version_label => 1)
|
32
|
+
assert_equal '1', @eb_driver.environment_verion_label('simple', 'production')
|
33
|
+
|
34
|
+
deploy(:application => 'simple',
|
35
|
+
:environment => "production",
|
36
|
+
:version_label => 2)
|
37
|
+
|
38
|
+
assert_equal '2', @eb_driver.environment_verion_label('simple', 'production')
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_default_cname_that_deployed_should_app_env_name
|
42
|
+
deploy(:application => 'simple',
|
43
|
+
:environment => "production",
|
44
|
+
:version_label => 42)
|
45
|
+
assert_equal "simple-production", @eb_driver.environment_cname_prefix('simple', 'production')
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_cname_prefix_can_be_override
|
49
|
+
deploy(:application => 'simple',
|
50
|
+
:environment => "production",
|
51
|
+
:cname_prefix => 'sports123',
|
52
|
+
:version_label => 42)
|
53
|
+
assert_equal "sports123", @eb_driver.environment_cname_prefix('simple', 'production')
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def test_smoke_test_should_be_run_after_env_created_or_update
|
58
|
+
host_for_smoke_test = nil
|
59
|
+
deploy(:application => 'simple',
|
60
|
+
:environment => "production",
|
61
|
+
:cname_prefix => 'foobar',
|
62
|
+
:smoke_test => lambda { |host| host_for_smoke_test = host },
|
63
|
+
:version_label => 42)
|
64
|
+
assert_equal 'foobar.elasticbeanstalk.com', host_for_smoke_test
|
65
|
+
|
66
|
+
host_for_smoke_test = nil
|
67
|
+
deploy(:application => 'simple',
|
68
|
+
:environment => "production",
|
69
|
+
:cname_prefix => 'foobar',
|
70
|
+
:smoke_test => lambda { |host| host_for_smoke_test = host },
|
71
|
+
:version_label => 43)
|
72
|
+
|
73
|
+
assert_equal 'foobar.elasticbeanstalk.com', host_for_smoke_test
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
|
78
|
+
def test_blue_green_deployment_strategy_should_create_blue_env_on_first_deployment
|
79
|
+
deploy(:application => 'simple',
|
80
|
+
:environment => "production",
|
81
|
+
:strategy => 'blue_green',
|
82
|
+
:version_label => 42)
|
83
|
+
|
84
|
+
assert @eb_driver.environment_exists?('simple', 'production-blue')
|
85
|
+
assert_equal 'simple-production', @eb_driver.environment_cname_prefix('simple', 'production-blue')
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def test_blue_green_deployment_should_create_green_env_if_blue_exists
|
90
|
+
deploy(:application => 'simple',
|
91
|
+
:environment => "production",
|
92
|
+
:strategy => 'blue_green',
|
93
|
+
:version_label => 42)
|
94
|
+
|
95
|
+
deploy(:application => 'simple',
|
96
|
+
:environment => "production",
|
97
|
+
:strategy => 'blue_green',
|
98
|
+
:version_label => 43)
|
99
|
+
|
100
|
+
assert @eb_driver.environment_exists?('simple', 'production-blue')
|
101
|
+
assert @eb_driver.environment_exists?('simple', 'production-green')
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def test_blue_green_deployment_should_swap_cname_to_make_active_most_recent_updated_env
|
106
|
+
deploy(:application => 'simple',
|
107
|
+
:environment => "production",
|
108
|
+
:strategy => 'blue_green',
|
109
|
+
:version_label => 42)
|
110
|
+
|
111
|
+
deploy(:application => 'simple',
|
112
|
+
:environment => "production",
|
113
|
+
:strategy => 'blue_green',
|
114
|
+
:version_label => 43)
|
115
|
+
|
116
|
+
assert_match(/simple-production-inactive/, @eb_driver.environment_cname_prefix('simple', 'production-blue'))
|
117
|
+
|
118
|
+
assert_equal 'simple-production', @eb_driver.environment_cname_prefix('simple', 'production-green')
|
119
|
+
|
120
|
+
|
121
|
+
deploy(:application => 'simple',
|
122
|
+
:environment => "production",
|
123
|
+
:strategy => 'blue_green',
|
124
|
+
:version_label => 44)
|
125
|
+
|
126
|
+
assert_match(/simple-production-inactive/, @eb_driver.environment_cname_prefix('simple', 'production-green'))
|
127
|
+
|
128
|
+
assert_equal 'simple-production', @eb_driver.environment_cname_prefix('simple', 'production-blue')
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
def test_blue_green_deploy_should_run_smoke_test_before_cname_switch
|
133
|
+
smoked_host = []
|
134
|
+
smoke_test = lambda { |host| smoked_host << host }
|
135
|
+
[42, 43, 44].each do |version_label|
|
136
|
+
deploy(:application => 'simple',
|
137
|
+
:environment => "production",
|
138
|
+
:strategy => 'blue_green',
|
139
|
+
:smoke_test => smoke_test,
|
140
|
+
:version_label => version_label)
|
141
|
+
end
|
142
|
+
|
143
|
+
assert_equal ['simple-production.elasticbeanstalk.com',
|
144
|
+
'simple-production-inactive.elasticbeanstalk.com',
|
145
|
+
'simple-production-inactive.elasticbeanstalk.com'], smoked_host
|
146
|
+
end
|
147
|
+
end
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: eb_deployer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- wpc
|
9
|
+
- betarelease
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-06-12 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: aws-sdk
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: minitest
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
type: :development
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
description: Elastic Beanstalk Deployer with different deployment strategies.
|
48
|
+
email:
|
49
|
+
- alex.hal9000@gmail.com
|
50
|
+
- sudhindra.r.rao@gmail.com
|
51
|
+
executables: []
|
52
|
+
extensions: []
|
53
|
+
extra_rdoc_files: []
|
54
|
+
files:
|
55
|
+
- .gitignore
|
56
|
+
- Gemfile
|
57
|
+
- LICENSE
|
58
|
+
- README.md
|
59
|
+
- Rakefile
|
60
|
+
- eb_deployer.gemspec
|
61
|
+
- lib/eb_deployer.rb
|
62
|
+
- lib/eb_deployer/beanstalk.rb
|
63
|
+
- lib/eb_deployer/cloud_formation_provisioner.rb
|
64
|
+
- lib/eb_deployer/deployment_strategy.rb
|
65
|
+
- lib/eb_deployer/environment.rb
|
66
|
+
- lib/eb_deployer/event_poller.rb
|
67
|
+
- lib/eb_deployer/package.rb
|
68
|
+
- lib/eb_deployer/s3_driver.rb
|
69
|
+
- lib/eb_deployer/version.rb
|
70
|
+
- test/aws_driver_stubs.rb
|
71
|
+
- test/deploy_test.rb
|
72
|
+
homepage: https://github.com/ThoughtWorksStudios/eb_deployer
|
73
|
+
licenses: []
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ! '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
requirements: []
|
91
|
+
rubyforge_project:
|
92
|
+
rubygems_version: 1.8.24
|
93
|
+
signing_key:
|
94
|
+
specification_version: 3
|
95
|
+
summary: Pick strategies like InplaceUpdate, Blue/Green.
|
96
|
+
test_files:
|
97
|
+
- test/aws_driver_stubs.rb
|
98
|
+
- test/deploy_test.rb
|