cloudfoundry_blue_green_deploy 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 54397c469bcc873ba16da8358aaf7659e5e0a7cd
4
+ data.tar.gz: 9f27071ae08a3dda2347f6371dddb242a6d8405e
5
+ SHA512:
6
+ metadata.gz: 924f6dfc9bf0c9e813629c0c5adccd0e6671be57c9d9c7c068c1074b8ecb4751b6348227c064e3b1b344f1fc1c97a9f1c3be38082dfbab9182210a9fa68d9c6d
7
+ data.tar.gz: e90a718e563b61cc450bc0c486c561812e26f09f5a84082a00e417bc4b145e29dc282072fac517d393dfbf1ccd31d5e4b6d3a3b79b3aa18b563926ed1a5d28bd
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cloudfoundry_blue_green_deploy.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 John Ryan and Mariana Lenetis
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,184 @@
1
+ # Overview
2
+
3
+ Using a simple deployment process, one can introduce significant (even if planned) downtime of your application.
4
+
5
+ If you want to minimize this impact to your site's availability, you might opt to use the Blue/Green deployment approach: http://docs.gopivotal.com/pivotalcf/devguide/deploy-apps/blue-green.html
6
+
7
+ This gem provides a Rake task to automate Blue/Green deployment to a Cloud Foundry installation.
8
+
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ gem 'cloudfoundry_blue_green_deploy'
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install cloudfoundry_blue_green_deploy
23
+
24
+ ## For first deployment - where app to deploy needs to be specified
25
+
26
+ ## Usage
27
+
28
+ 1. define the blue and green instances of your application(s) in your Cloud Foundry Manifest. (see rules in the next section)
29
+ 2. run
30
+
31
+ $ bundle exec rake cf:blue_green_deploy[web-app-name]
32
+
33
+ Where "web-app-name" is the "name" attribute in your manifest.yml.
34
+ The default color for first deployment is blue.
35
+ You may optionally specify the color that you would like to be the "live" instance on the first deployment.
36
+
37
+ ### manifest.yml
38
+
39
+ Your Cloud Foundry manifest file must comply with the following requirements:
40
+
41
+ 1. name: two application instances are required. One name ending with "-green" and another ending with "-blue"
42
+ 2. host: the url. One ending with "-green" and the other ending with "-blue"
43
+ 3. domain: required
44
+ 4. command: bundle exec rake cf:on_first_instance db:migrate && bundle exec rails s -p $PORT -e $RAILS_ENV
45
+ 5. services: Optional, only required if there are services that need to be bound
46
+
47
+ #### Bare Minimum Example:
48
+
49
+ In this example:
50
+ - Our web application is known to Cloud Foundry as "carrot-soup".
51
+ - "carrot-soup" has a database service known as "oyster-cracker".
52
+
53
+ ---
54
+ applications:
55
+
56
+ - name: carrot-soup-green
57
+ host: la-pong-green
58
+ domain: cfapps.io
59
+ command: bundle exec rake cf:on_first_instance db:migrate && bundle exec rails s -p $PORT -e $RAILS_ENV
60
+ services:
61
+ - oyster-cracker
62
+
63
+ - name: carrot-soup-blue
64
+ host: la-pong-blue
65
+ domain: cfapps.io
66
+ command: bundle exec rake cf:on_first_instance db:migrate && bundle exec rails s -p $PORT -e $RAILS_ENV
67
+ services:
68
+ - oyster-cracker
69
+
70
+ And perform a blue/green deploy like this:
71
+
72
+ $ bundle exec rake cf:blue_green_deploy[carrot-soup]
73
+
74
+ ## Workers
75
+
76
+ Non-trivial applications often require background processes to perform asynchronous jobs (e.g. sending email, importing data from external systems, etc.).
77
+ If these applications' code are to stay in sync with the web application, they need blue/green treatment as well.
78
+
79
+ This Rake task natively supports worker application instances.
80
+
81
+ ### Usage (with Workers)
82
+
83
+ 1. define the blue and green instances of your application(s) and workers in your Cloud Foundry Manifest. (see rules in the next section)
84
+ 2. run:
85
+
86
+ $ bundle exec rake cf:blue_green_deploy[web-app-name,worker-name,another-worker-name]
87
+
88
+ Note:
89
+ The "web-app-name" is the "name" attribute (without a color) detailed in your manifest.yml
90
+ The "worker-name" and "another-worker-name" are "name" attributes for 2 separate worker apps as detailed in your manifest.yml
91
+ Multiple worker apps can be specified as long as they comply with the blue/green deployment requirements in the manifest.yml
92
+
93
+ ### manifest.yml
94
+
95
+ For web application deployment (see requirements above)
96
+
97
+ For worker applications
98
+ 1. name: two application instances are required. One name ending with "-green" and another ending with "-blue"
99
+ 2. command:
100
+ 3. path: Relative to the current working directory
101
+ 4. services: Optional, only required if there are services that need to be bound
102
+
103
+ #### Example with Workers
104
+
105
+ In this example:
106
+ - Our web application is known to Cloud Foundry as "carrot-soup".
107
+ - The app "carrot-soup" has a database service known as "oyster-cracker".
108
+ - We have a worker application named "relish", whose database is known as "creme-fraiche".
109
+
110
+ ---
111
+ applications:
112
+
113
+ - name: relish-green
114
+ command: bundle exec rails s -p $PORT -e $RAILS_ENV
115
+ path: ../relish
116
+ services:
117
+ - creme-fraiche
118
+
119
+ - name: carrot-soup-green
120
+ host: la-pong-green
121
+ domain: cfapps.io
122
+ size: 1GB
123
+ path: .
124
+ command: bundle exec rails s -p $PORT -e $RAILS_ENV
125
+ services:
126
+ - oyster-cracker
127
+
128
+ - name: carrot-soup-blue
129
+ host: la-pong-blue
130
+ domain: cfapps.io
131
+ size: 1GB
132
+ path: .
133
+ command: bundle exec rails s -p $PORT -e $RAILS_ENV
134
+ services:
135
+ - oyster-cracker
136
+
137
+ - name: relish-blue
138
+ command: bundle exec rails s -p $PORT -e $RAILS_ENV
139
+ path: ../relish
140
+ services:
141
+ - creme-fraiche
142
+
143
+ And perform the blue/green deploy like this:
144
+ $ bundle exec rake cf:blue_green_deploy[carrot-soup,relish]
145
+
146
+
147
+ # Blue/Green with Shutter
148
+
149
+ For blue/green deployments that require a database migration this tool provides the ability to automatically shutter the app during the required downtime. To use this feature, create a shutter app and configure your manifest.yml.
150
+
151
+ ## Creating a Minimal Shutter App
152
+
153
+ 1. Add the following to your manifest.yml. Note that the name must match the name of your production application and end in -shutter.
154
+
155
+ - name: carrot-soup-shutter
156
+ command: bundle exec rackup config.ru -p $PORT -E $RACK_ENV
157
+ path: shutter-app
158
+
159
+ 2. Create a directory named "shutter-app". In that directory:
160
+ 1. create a Rack config (config.ru):
161
+
162
+ class Message
163
+ def call(env)
164
+ [200, {"Content-Type" => "text/plain"}, ["Temporarily down for maintenance. Please check back shortly."]]
165
+ end
166
+ end
167
+
168
+ run Message.new
169
+
170
+ 2. create a minimal Gemfile:
171
+
172
+ source 'https://rubygems.org'
173
+ ruby '2.0.0'
174
+
175
+ gem 'rack'
176
+
177
+ 3. create the Gemfile.lock by running Bundler in the "shutter-app" directory:
178
+
179
+ $ bundle install
180
+
181
+
182
+ - Note: as of 05/09/14 deployment using Cloud Foundry's buildpack does not appear to be compatible with ruby version 2.1.0.
183
+ - Our fail-fast philosophy. We recommend understanding deployment on Cloud Foundry before using this tool.
184
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cloudfoundry_blue_green_deploy/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'cloudfoundry_blue_green_deploy'
8
+ spec.version = CloudfoundryBlueGreenDeploy::VERSION
9
+ spec.authors = ['John Ryan and Mariana Lenetis']
10
+ spec.email = ['jryan@pivotallabs.com', 'mlenetis@pivotallabs.com']
11
+ spec.summary = %q{Blue-green deployment tool for Cloud Foundry.}
12
+ spec.description = %q{Blue-green deployment tool for Cloud Foundry. Please see readme.}
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.6'
22
+ spec.add_development_dependency 'rake'
23
+ spec.add_development_dependency 'rspec'
24
+ spec.add_development_dependency 'awesome_print'
25
+ end
@@ -0,0 +1,8 @@
1
+ require_relative 'cloudfoundry_blue_green_deploy/app'
2
+ require_relative 'cloudfoundry_blue_green_deploy/route'
3
+ require_relative 'cloudfoundry_blue_green_deploy/command_line'
4
+ require_relative 'cloudfoundry_blue_green_deploy/cloudfoundry'
5
+ require_relative 'cloudfoundry_blue_green_deploy/blue_green_deploy_error'
6
+ require_relative 'cloudfoundry_blue_green_deploy/blue_green_deploy_config'
7
+ require_relative 'cloudfoundry_blue_green_deploy/blue_green_deploy'
8
+ require_relative 'cloudfoundry_blue_green_deploy/railtie' if defined?(Rails)
@@ -0,0 +1,10 @@
1
+ module CloudfoundryBlueGreenDeploy
2
+ class App
3
+ attr_accessor :name, :state
4
+
5
+ def initialize(name: , state: 'stopped')
6
+ @name = name
7
+ @state = state
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,144 @@
1
+ require_relative 'route'
2
+ require_relative 'cloudfoundry'
3
+ require_relative 'blue_green_deploy_error'
4
+ require_relative 'blue_green_deploy_config'
5
+
6
+ module CloudfoundryBlueGreenDeploy
7
+ class InvalidRouteStateError < BlueGreenDeployError; end
8
+ class InvalidWorkerStateError < BlueGreenDeployError; end
9
+
10
+ class BlueGreenDeploy
11
+ def self.cf
12
+ Cloudfoundry
13
+ end
14
+
15
+ def self.make_it_so(app_name, worker_apps, deploy_config)
16
+ hot_app_name = get_hot_web_app(deploy_config.hot_url)
17
+ both_invalid_and_valid_hot_worker_names = get_hot_worker_names
18
+
19
+ is_first_deploy = first_deploy?(hot_app_name, both_invalid_and_valid_hot_worker_names)
20
+
21
+ deploy_config.target_color = set_target_color(is_first_deploy, hot_app_name)
22
+
23
+ ready_for_takeoff(hot_app_name, both_invalid_and_valid_hot_worker_names, deploy_config)
24
+
25
+ with_shutter = deploy_config.with_shutter
26
+ hot_url = deploy_config.hot_url
27
+ cold_app = deploy_config.target_web_app_name
28
+ domain = deploy_config.domain
29
+ shutter_app_name = deploy_config.shutter_app_name
30
+
31
+ if with_shutter
32
+ cf.push(shutter_app_name)
33
+ cf.map_route(shutter_app_name, domain, hot_url)
34
+ cf.unmap_route(hot_app_name, domain, hot_url) if hot_app_name
35
+ end
36
+
37
+ cf.push(cold_app)
38
+
39
+ deploy_config.target_worker_app_names.each do |worker_app_name|
40
+ cf.push(worker_app_name)
41
+ unless is_first_deploy
42
+ to_be_cold_worker = BlueGreenDeployConfig.toggle_app_color(worker_app_name)
43
+ cf.stop(to_be_cold_worker)
44
+ end
45
+ end
46
+
47
+ if with_shutter
48
+ cf.map_route(cold_app, domain, hot_url)
49
+ cf.unmap_route(shutter_app_name, domain, hot_url)
50
+ else
51
+ make_hot(app_name, deploy_config)
52
+ end
53
+ end
54
+
55
+ def self.set_target_color(is_first_deploy, hot_app_name)
56
+ if is_first_deploy
57
+ 'blue'
58
+ else
59
+ determine_target_color(hot_app_name) unless hot_app_name.nil?
60
+ end
61
+ end
62
+
63
+
64
+ def self.ready_for_takeoff(hot_app_name, both_invalid_and_valid_hot_worker_names, deploy_config)
65
+ unless first_deploy?(hot_app_name, both_invalid_and_valid_hot_worker_names)
66
+ ensure_there_is_a_hot_instance(deploy_config, hot_app_name)
67
+ ensure_hot_instance_is_not_target(deploy_config, hot_app_name)
68
+ ensure_hot_workers_are_not_target(deploy_config)
69
+ end
70
+ end
71
+
72
+ def self.first_deploy?(hot_app_name, both_invalid_and_valid_hot_worker_names)
73
+ hot_app_name.nil? && both_invalid_and_valid_hot_worker_names.empty?
74
+ end
75
+
76
+ def self.ensure_there_is_a_hot_instance(deploy_config, hot_app_name)
77
+ if hot_app_name.nil?
78
+ raise InvalidRouteStateError.new(
79
+ "There is no route mapped from #{deploy_config.hot_url} to an app.")
80
+ end
81
+ end
82
+
83
+ def self.ensure_hot_instance_is_not_target(deploy_config, hot_app_name)
84
+ if deploy_config.is_in_target?(hot_app_name)
85
+ raise InvalidRouteStateError.new(
86
+ "The app \"#{hot_app_name}\" is already hot (target color is #{deploy_config.target_color}).")
87
+ end
88
+ end
89
+
90
+ def self.ensure_hot_workers_are_not_target(deploy_config)
91
+ apps = cf.apps
92
+
93
+ deploy_config.target_worker_app_names.each do |hot_worker|
94
+ if deploy_config.is_in_target?(hot_worker) && invalid_worker?(hot_worker, apps)
95
+ raise InvalidWorkerStateError.new(
96
+ "Worker #{hot_worker} is already hot (target color is #{deploy_config.target_color}).")
97
+ end
98
+ end
99
+ end
100
+
101
+ def self.invalid_worker?(hot_worker, apps)
102
+ apps.each do |app|
103
+ if app.name == hot_worker && app.state == 'started'
104
+ return true
105
+ end
106
+ end
107
+ return false
108
+ end
109
+
110
+ def self.get_color_stem(hot_app_name)
111
+ hot_app_name.slice((hot_app_name.rindex('-') + 1)..(hot_app_name.length))
112
+ end
113
+
114
+ def self.determine_target_color(hot_app_name)
115
+ target_color = get_color_stem(hot_app_name)
116
+ BlueGreenDeployConfig.toggle_color(target_color)
117
+ end
118
+
119
+ def self.make_hot(app_name, deploy_config)
120
+ hot_url = deploy_config.hot_url
121
+ hot_app = get_hot_web_app(hot_url)
122
+ cold_app = deploy_config.target_web_app_name
123
+ domain = deploy_config.domain
124
+
125
+ cf.map_route(cold_app, domain, hot_url)
126
+ cf.unmap_route(hot_app, domain, hot_url) if hot_app
127
+ end
128
+
129
+ def self.get_hot_web_app(hot_url)
130
+ cf_routes = cf.routes
131
+ hot_route = cf_routes.find { |route| route.host == hot_url }
132
+ hot_route.nil? ? nil : hot_route.app
133
+ end
134
+
135
+ def self.get_hot_worker_names
136
+ cf_apps = cf.apps
137
+ hot_names = []
138
+ cf_apps.each do |app|
139
+ hot_names << app.name if app.state == 'started'
140
+ end
141
+ hot_names
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,108 @@
1
+ require_relative 'blue_green_deploy_error'
2
+
3
+ module CloudfoundryBlueGreenDeploy
4
+ class InvalidManifestError < BlueGreenDeployError; end
5
+
6
+ class BlueGreenDeployConfig
7
+ attr_reader :hot_url, :worker_app_names, :domain, :with_shutter
8
+ attr_accessor :target_color
9
+
10
+ def initialize(cf_manifest, web_app_name, worker_app_names, with_shutter = nil)
11
+ manifest = cf_manifest['applications']
12
+
13
+ self.class.valid_name_check(web_app_name, worker_app_names, manifest)
14
+
15
+ item = manifest.find { |item| self.class.strip_color(item['name']) == web_app_name }
16
+ if item.nil?
17
+ raise InvalidManifestError.new("Could not find \"#{web_app_name}-green\" nor \"#{web_app_name}-blue\" in the Cloud Foundry manifest:\n" +
18
+ "#{cf_manifest.inspect}")
19
+ end
20
+
21
+ host = item['host']
22
+ if host.nil?
23
+ raise InvalidManifestError.new(
24
+ "Could not find the \"host\" property associated with the \"#{item['name']}\" application in the Cloud Foundry manifest:\n" +
25
+ "#{cf_manifest.inspect}")
26
+ end
27
+
28
+ @domain = item['domain']
29
+
30
+ if @domain.nil?
31
+ raise InvalidManifestError.new(
32
+ "Could not find the \"domain\" property associated with the \"#{item['name']}\" application in the Cloud Foundry manifest:\n" +
33
+ "#{cf_manifest.inspect}")
34
+ end
35
+
36
+ @web_app_name = web_app_name
37
+ @hot_url = host.slice(0, host.rindex('-'))
38
+ @worker_app_names = worker_app_names
39
+ @with_shutter = with_shutter
40
+ @target_color = nil
41
+ end
42
+
43
+ def shutter_app_name
44
+ "#{@web_app_name}-shutter"
45
+ end
46
+
47
+ def target_web_app_name
48
+ "#{@web_app_name}-#{@target_color}"
49
+ end
50
+
51
+ def is_in_target?(app)
52
+ self.class.get_color_stem(app) == @target_color
53
+ end
54
+
55
+ def target_worker_app_names
56
+ @worker_app_names.map do |app|
57
+ "#{app}-#{@target_color}"
58
+ end
59
+ end
60
+
61
+
62
+ def self.valid_name_check(web_app_name, worker_app_names, manifest)
63
+ all_apps = all_app_names(web_app_name, worker_app_names)
64
+ all_apps.each do |app_name|
65
+ if manifest.none? { |record| record['name'] == app_name }
66
+ raise InvalidManifestError.new("Could not find \"#{app_name}\" in the Cloud Foundry manifest:\n" +
67
+ "#{manifest}")
68
+
69
+ end
70
+ end
71
+ end
72
+
73
+ def self.strip_color(app_name_with_color)
74
+ app_name_with_color.slice((0..app_name_with_color.rindex('-') - 1))
75
+ end
76
+
77
+ def self.toggle_app_color(target_app_name)
78
+ new_color = toggle_color(get_color_stem(target_app_name))
79
+ new_app = target_app_name.slice(0..(target_app_name.rindex('-') - 1))
80
+ new_app = "#{new_app}-#{new_color}"
81
+ end
82
+
83
+ def self.get_color_stem(app_name)
84
+ app_name.slice((app_name.rindex('-') + 1)..(app_name.length))
85
+ end
86
+
87
+ def self.toggle_color(target_color)
88
+ target_color == 'green' ? 'blue' : 'green'
89
+ end
90
+
91
+ def self.colorize_name(app_name, color)
92
+ "#{app_name}-#{color}"
93
+ end
94
+
95
+ def self.all_app_names(web_app_name, worker_app_names)
96
+ all_app_names = []
97
+ all_app_names << colorize_name(web_app_name, 'blue')
98
+ all_app_names << colorize_name(web_app_name, 'green')
99
+
100
+ worker_app_names.each do |app|
101
+ all_app_names << colorize_name(app, 'green')
102
+ all_app_names << colorize_name(app, 'blue')
103
+ end
104
+
105
+ all_app_names
106
+ end
107
+ end
108
+ end