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