opsworks_wrapper 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 382ccf56835c9a6787c03a12a2de311989492b72
4
- data.tar.gz: 3efeb0cacd432e2e87f8b5d3f34945b289959c5b
3
+ metadata.gz: 40e9f15632090461fc0c1243cd6dd94370882945
4
+ data.tar.gz: 247915c8d6b5105abdabf2b960aa7650a0414268
5
5
  SHA512:
6
- metadata.gz: fae78c1576999010eb4ac8dfce4582f33d631da2445fec1f8839ac2ef744c1e711644b04ce2b34012dde28478ebde107ea4dde37c1f7e96de4ad405f2552f5d0
7
- data.tar.gz: e227d5f12324eb3ef06d5f5b68e7bb38499bb1ad69d09bb2513a4eb2d0824f0f4e891d72b645fa0046fa89f91a77dceb8cbaa83f673bc50519e20495397eade6
6
+ metadata.gz: 5666992cdb2760f699dc2bdc671408734ca9fadfda59c79be5ac09ea6eb0c75c165ede33994a21b45dec1c6b0a9bd08ef312adbbf1cb1cea3e7a0c680c72d96d
7
+ data.tar.gz: 3bf986520f92cd8a7edd3ec2ecafea23278f6f1a0e574be6c4f1a40c6fed464e16634a9de7a05eecbe2142629018f1d3c618477639d5943d875e29fc777f3a87
data/README.md CHANGED
@@ -8,8 +8,6 @@ A simple wrapper for aws-sdk to make handling opsworks operations easier.
8
8
  - Deployment Creation for all layers except a specified layer (exclusion by layer name)
9
9
  - Retrieve all instances for a given layer name
10
10
 
11
- TODO: Delete this and the text above, and describe your gem
12
-
13
11
  ## Installation
14
12
 
15
13
  Add this line to your application's Gemfile:
@@ -17,8 +17,12 @@ module OpsworksWrapper
17
17
  @opsworks_app ||= get_opsworks_app
18
18
  end
19
19
 
20
- def client
21
- @client ||= Aws::OpsWorks::Client.new
20
+ def opsworks_client
21
+ @opsworks_client ||= Aws::OpsWorks::Client.new
22
+ end
23
+
24
+ def load_balancer_client
25
+ @client ||= Aws::ElasticLoadBalancing::Client.new
22
26
  end
23
27
 
24
28
  def app_id
@@ -29,8 +33,10 @@ module OpsworksWrapper
29
33
  @layers ||= get_opsworks_layers
30
34
  end
31
35
 
36
+ # Returns a dictionary for all OpsWorks layers keyed by layer name
37
+ # @return [Dictionary]
32
38
  def get_opsworks_layers
33
- data = client.describe_layers(stack_id: opsworks_app[:stack_id])
39
+ data = opsworks_client.describe_layers(stack_id: opsworks_app[:stack_id])
34
40
  layers = {}
35
41
  data.layers.each do |layer|
36
42
  layers[layer.name] = layer
@@ -38,59 +44,143 @@ module OpsworksWrapper
38
44
  layers
39
45
  end
40
46
 
47
+ # Returns OpsWorks app details
48
+ # @return [Object]
41
49
  def get_opsworks_app
42
- data = client.describe_apps(app_ids: [app_id])
43
- unless data[:apps] && data[:apps].count == 1
50
+ data = opsworks_client.describe_apps(app_ids: [app_id])
51
+ if !(data[:apps] && data[:apps].count == 1)
44
52
  raise Error, "App #{app_id} not found.", error.backtrace
45
53
  end
46
54
  data[:apps].first
47
55
  end
48
56
 
57
+
58
+ # Returns a list of OpsWorks instances for a specific layer or all layers if @layer_name is not provided
59
+ # @param [String] layer_name
60
+ # @return [List[Object]] - List of OpsWorks instances
49
61
  def get_instances(layer_name = nil)
50
62
  if layer_name == nil
51
- data = client.describe_instances(stack_id: opsworks_app[:stack_id])
63
+ data = opsworks_client.describe_instances(stack_id: opsworks_app[:stack_id])
52
64
  else
53
65
  layer_id = layers[layer_name].layer_id
54
- data = client.describe_instances(layer_id: layer_id)
66
+ data = opsworks_client.describe_instances(layer_id: layer_id)
55
67
  end
56
68
 
57
69
  data.instances
58
70
  end
59
71
 
60
- # update cookbooks on all layers
72
+ # Returns ELB instance for layer if one is attached
73
+ # @param [String] layer_name
74
+ # @return [Object?] - ELB instance
75
+ def get_elb(layer_name)
76
+ layer_id = layers[layer_name].layer_id
77
+ elbs = opsworks_client.describe_elastic_load_balancers(layer_ids:[layer_id])
78
+ if elbs.elastic_load_balancers.size > 0
79
+ name = elbs.elastic_load_balancers.first.elastic_load_balancer_name
80
+ ELB.new(name)
81
+ else
82
+ nil
83
+ end
84
+ end
85
+
86
+ # Run update cookbooks on all layers
87
+ # @param [Number] timeout
88
+ # @return [Boolean]
61
89
  def update_cookbooks(timeout = 150)
62
90
  puts 'Updating cookbooks'.light_white.bold
63
91
  create_deployment({name: 'update_custom_cookbooks'}, nil, timeout)
64
92
  end
65
93
 
66
- # deploy to specified layer or all layers (default)
94
+ # Run deploy command on specified layer or all layers if @layer_name is not specified (non rolling)
95
+ # @param [String] layer_name
96
+ # @param [Number] timeout
97
+ # @return [Boolean]
67
98
  def deploy(layer_name = nil, timeout = 600)
68
99
  if layer_name
69
100
  puts "Deploying on #{layer_name} layer".light_white.bold
70
- instance_ids = get_instances(layer_name).map(&:instance_id)
101
+ instances = get_instances(layer_name)
71
102
  else
72
103
  puts "Deploying on all layers".light_white.bold
73
- instance_ids = nil
104
+ instances = nil
105
+ end
106
+
107
+ create_deployment({name: 'deploy'}, instances, timeout)
108
+ end
109
+
110
+ # Performs a rolling deploy on each instance in the layer
111
+ # If an elb is attached to the layer, de-registration and registration will be performed for the instance
112
+ # @param [String] layer_name
113
+ # @param [Number] timeout
114
+ # @return [Boolean]
115
+ def deploy_layer_rolling(layer_name, timeout = 600)
116
+ instances = get_instances(layer_name)
117
+ elb = get_elb(layer_name)
118
+ success = true
119
+ instances.each do |instance|
120
+ success = deploy_instance_rolling(instance, elb, timeout)
121
+ break if !success
122
+ end
123
+ success
124
+ end
125
+
126
+ # Performs rolling deployment on an instance
127
+ # Will detach instance if elb is provided and re-attach after deployment succeeds
128
+ # @param [Object] instance - opsworks instance
129
+ # @param [Object] elb - elb instance
130
+ # @param [Number] timeout
131
+ # @return [Boolean]
132
+ def deploy_instance_rolling(instance, elb, timeout = 600)
133
+ if !elb.nil?
134
+ elb.remove_instance(instance)
74
135
  end
75
136
 
76
- create_deployment({name: 'deploy'}, instance_ids, timeout)
137
+ success = create_deployment({name: 'deploy'}, [instance], timeout)
138
+
139
+ # only add instance back to elb if deployment succeeded
140
+ if !elb.nil? && success
141
+ success = elb.add_instance(instance)
142
+ end
143
+
144
+ success
77
145
  end
78
146
 
79
- # deploy to all layers except specified layer
147
+ # Deploy to all layers except specified layer (non-rolling)
148
+ # @param [String] layer_name
149
+ # @param [Number] timeout
150
+ # @return [Boolean]
80
151
  def deploy_exclude(layer_name, timeout = 600)
81
152
  puts "Deploying to all layers except #{layer_name}".light_white.bold
82
153
  create_deployment_exclude({name: 'deploy'}, layer_name, timeout)
83
154
  end
84
155
 
156
+ # Creates an OpsWorks deployment with specified command on all layers excluding layer_to_exclude
157
+ # @param [Object] command - Opsworks deployment command
158
+ # @param [String] layer_to_exclude
159
+ # @param [Number] timeout
160
+ # @return [Boolean]
85
161
  def create_deployment_exclude(command, layer_to_exclude, timeout)
86
- all_instance_ids = get_instances.map(&:instance_id)
87
- excluded_instance_ids = get_instances(layer_to_exclude).map(&:instance_id)
88
- included_instance_ids = all_instance_ids - excluded_instance_ids
162
+ all_instances = get_instances
163
+ excluded_instances = get_instances(layer_to_exclude)
164
+ included_instances = all_instances - excluded_instances
89
165
 
90
- create_deployment(command, included_instance_ids, timeout)
166
+ create_deployment(command, included_instances, timeout)
91
167
  end
92
168
 
93
- def create_deployment(command, instance_ids, timeout)
169
+ # Creates an OpsWorks deployment with specified command
170
+ # If @instances is not nil, the deployment will only be performed on specified instances
171
+ # @param [Object] command
172
+ # @param [Array[Object]] instances
173
+ # @param [Number] timeout
174
+ # @return [Boolean]
175
+ def create_deployment(command, instances, timeout)
176
+ instance_ids = nil
177
+ instance_description = "all instances"
178
+
179
+ if !instances.nil?
180
+ instance_ids = instances.map(&:instance_id)
181
+ instance_description = instances.map(&:hostname).join(',')
182
+ end
183
+
94
184
  deployment_config = {
95
185
  stack_id: opsworks_app[:stack_id],
96
186
  app_id: app_id,
@@ -99,12 +189,13 @@ module OpsworksWrapper
99
189
  comment: "Git Sha: #{current_sha}"
100
190
  }
101
191
 
102
- deployment = client.create_deployment(deployment_config)
103
- puts "Deployment created: #{deployment[:deployment_id]}".blue
104
- puts "Running Command: #{command[:name]} ".light_blue
192
+ deployment = opsworks_client.create_deployment(deployment_config)
193
+ print "Running command ".light_blue
194
+ print "#{command[:name]}".light_blue.bold
195
+ puts " on #{instance_description}".light_blue
105
196
 
106
197
  begin
107
- wait_until_deployed(deployment[:deployment_id], timeout)
198
+ _wait_until_deployed(deployment[:deployment_id], timeout)
108
199
  puts "Deployment successful".green
109
200
  true
110
201
  rescue Aws::Waiters::Errors::WaiterFailed => e
@@ -114,8 +205,8 @@ module OpsworksWrapper
114
205
  end
115
206
 
116
207
  # Waits on the provided deployment for specified timeout (seconds)
117
- def wait_until_deployed(deployment_id, timeout)
118
- client.wait_until(:deployment_successful, deployment_ids: [deployment_id]) do |w|
208
+ def _wait_until_deployed(deployment_id, timeout)
209
+ opsworks_client.wait_until(:deployment_successful, deployment_ids: [deployment_id]) do |w|
119
210
  w.before_attempt do |attempt|
120
211
  puts "Attempt #{attempt} to check deployment status".light_black
121
212
  end
@@ -123,7 +214,110 @@ module OpsworksWrapper
123
214
  w.max_attempts = timeout / w.interval
124
215
  end
125
216
  end
126
- private :wait_until_deployed
217
+
218
+ end
219
+
220
+ class ELB
221
+ require 'aws-sdk'
222
+ require 'colorize'
223
+
224
+ def initialize(name)
225
+ @name = name
226
+ end
227
+
228
+ def name
229
+ @name
230
+ end
231
+
232
+ def client
233
+ @client ||= Aws::ElasticLoadBalancing::Client.new
234
+ end
235
+
236
+ def attributes
237
+ @attributes ||= client.describe_load_balancer_attributes(load_balancer_name: name)
238
+ end
239
+
240
+ def health_check
241
+ elb = client.describe_load_balancers(load_balancer_names: [name]).load_balancer_descriptions.first
242
+ @health_check ||= elb.health_check
243
+ end
244
+
245
+ # Determines if elb has connection draining enabled and waits for the timeout period or (20s) default
246
+ def _wait_for_connection_draining
247
+ connection_draining = attributes.load_balancer_attributes.connection_draining
248
+ if connection_draining.enabled
249
+ timeout = connection_draining.timeout
250
+ puts "Connection Draining Enabled - sleeping for #{timeout}".light_black
251
+ sleep(timeout)
252
+ else
253
+ puts "Connection Draining Disabled - sleeping for 20 seconds".light_black
254
+ sleep(20)
255
+ end
256
+ end
257
+
258
+ # Waits on instance to be in service according to the ELB
259
+ # @param [Object] instance
260
+ # @return [Boolean]
261
+ def _wait_for_instance_health_check(instance)
262
+ health_threshold = health_check.healthy_threshold
263
+ interval = health_check.interval
264
+
265
+ # wait a little longer than the defined threshold to account for application launch time
266
+ timeout = ((health_threshold + 2) * interval)
267
+
268
+ begin
269
+ client.wait_until(:instance_in_service, {load_balancer_name: name,
270
+ instances: [{instance_id: instance.ec2_instance_id}]}) do |w|
271
+ w.before_attempt do |attempt|
272
+ puts "Attempt #{attempt} to check health status for #{instance.hostname}".light_black
273
+ end
274
+ w.interval = 10
275
+ w.max_attempts = timeout / w.interval
276
+ end
277
+ puts "Instance #{instance.hostname} is now InService".green
278
+ true
279
+ rescue Aws::Waiters::Errors::WaiterFailed => e
280
+ puts "Instance #{instance.hostname} failed to move to InService, #{e.message}".red
281
+ false
282
+ end
283
+
284
+ end
285
+
286
+ # Removes instance from ELB and waits for connection draining
287
+ # @param [Object] instance - object with ec2_instance_id and hostname
288
+ def remove_instance(instance)
289
+ deregister_response = client.deregister_instances_from_load_balancer(load_balancer_name: name,
290
+ instances: [{instance_id: instance.ec2_instance_id}])
291
+ remaining_instance_count = deregister_response.instances.size
292
+ puts "Removed #{instance.hostname} from ELB #{name}. Remaining instances: #{remaining_instance_count}".light_blue
293
+ _wait_for_connection_draining
294
+ end
295
+
296
+ # Adds instance to ELB and waits for instance health check to pass
297
+ # @param [Object] instance - object with ec2_instance_id and hostname
298
+ # @return [Boolean]
299
+ def add_instance(instance)
300
+ register_response = client.register_instances_with_load_balancer(load_balancer_name: name,
301
+ instances: [{instance_id: instance.ec2_instance_id}])
302
+ remaining_instance_count = register_response.instances.size
303
+ puts "Added #{instance.hostname} to ELB #{name}. Attached instances: #{remaining_instance_count}".light_blue
304
+ _wait_for_instance_health_check(instance)
305
+ end
306
+
307
+ # Checks whether an instance attached to ELB is healthy
308
+ # @param [Object] instance - object with ec2_instance_id and hostname
309
+ # @return [Boolean]
310
+ def is_instance_healthy(instance)
311
+ instance_health = client.describe_instance_health(load_balancer_name: name,
312
+ instances: [{instance_id: instance.ec2_instance_id}])
313
+ state_info = instance_health.instance_states.first
314
+ status_detail = ''
315
+ if state_info.state != 'InService'
316
+ status_detail = "#{state_info.reason_code} - #{state_info.description}."
317
+ end
318
+ puts "Instance state is #{state_info.state} #{status_detail}"
319
+ state_info.state == 'InService'
320
+ end
127
321
 
128
322
  end
129
323
  end
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "opsworks_wrapper"
7
- spec.version = '0.1.0'
7
+ spec.version = '0.2.0'
8
8
  spec.authors = ["Umair Kayani", "Calvin Fernandes"]
9
9
  spec.email = ["ukayani@loyalty.com", "cfernandes@loyalty.com"]
10
10
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opsworks_wrapper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Umair Kayani
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-04-15 00:00:00.000000000 Z
12
+ date: 2016-04-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: aws-sdk
@@ -82,7 +82,6 @@ files:
82
82
  - README.md
83
83
  - Rakefile
84
84
  - lib/opsworks_wrapper.rb
85
- - lib/opsworks_wrapper/version.rb
86
85
  - opsworks_wrapper.gemspec
87
86
  homepage: https://github.com/ukayani/opsworks-wrapper
88
87
  licenses:
@@ -1,3 +0,0 @@
1
- module OpsworksWrapper
2
- VERSION = "0.1.0"
3
- end