capistrano-asg-rolling 0.2.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 201b0bf6f65154de3bef482b74683de12e689957f2776924b5b6b6187eb8523e
4
- data.tar.gz: 182402f95b0dd18f6376566632b0712b8f049ca8fd67f454ce750be296e3cc5d
3
+ metadata.gz: 517d95aa2350605f6a9fb472c3c5b293916946747936f2850219f65f6eb4b439
4
+ data.tar.gz: 159c94f7e0c371d0e7d1877fbc73ad40e923fc6e8c24e22ab586b7c603c98999
5
5
  SHA512:
6
- metadata.gz: cfa3660b65b46f41fabe64ec8a5cc052189e4752ae2ac4a6635055c2b143595d0c402773e1255ddc6ff0b0ed3d6d83a53742abc5ed4ac7b42170b0ca268837a0
7
- data.tar.gz: 36d6647e1bf5524b488c5b61dcd65cd6b14087d1ea8110a191496bc7127a0091002295375a380e3210622ded106a54442533c57443b0122b8f57f42d3bab5f5f
6
+ metadata.gz: 628c4200fb0e4e81370829d700bc0206a81390ad4392d97459c20f8c358a72a1882bcacd829cb69dcb816f9c2f5ed39ef5f8c05c35e7abc7231defa41fce98c0
7
+ data.tar.gz: 688a2886933cb90fd2781946e09fd54bd8d72fbbe450256fdd8c16302465007652c34194047a9a49e87226a7a0f823aeba6f5ddc9f210102edf1e6622afa495c
@@ -9,7 +9,7 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  strategy:
11
11
  matrix:
12
- ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2']
12
+ ruby-version: ['2.7', '3.0', '3.1', '3.2']
13
13
 
14
14
  steps:
15
15
  - uses: actions/checkout@v2
data/.rubocop.yml CHANGED
@@ -5,10 +5,13 @@ require:
5
5
 
6
6
  AllCops:
7
7
  NewCops: enable
8
- TargetRubyVersion: 2.6
8
+ TargetRubyVersion: 2.7
9
9
  DisplayCopNames: true
10
10
  DisplayStyleGuide: true
11
11
 
12
+ Gemspec/DevelopmentDependencies:
13
+ EnforcedStyle: gemspec
14
+
12
15
  Layout/LineLength:
13
16
  Enabled: false
14
17
 
@@ -18,11 +21,17 @@ Metrics/AbcSize:
18
21
  Metrics/BlockLength:
19
22
  Enabled: false
20
23
 
24
+ Metrics/ClassLength:
25
+ Enabled: false
26
+
21
27
  Metrics/MethodLength:
22
28
  Enabled: false
23
29
 
24
30
  RSpec/ExampleLength:
25
31
  Max: 9
26
32
 
33
+ RSpec/IndexedLet:
34
+ Enabled: false
35
+
27
36
  RSpec/MultipleExpectations:
28
37
  Max: 5
data/Gemfile CHANGED
@@ -7,7 +7,7 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
7
7
  # Specify your gem's dependencies in capistrano-asg-rolling.gemspec
8
8
  gemspec
9
9
 
10
- gem 'rubocop', '~> 1.50.2'
11
- gem 'rubocop-performance', '~> 1.17.0'
10
+ gem 'rubocop', '~> 1.56.3'
11
+ gem 'rubocop-performance', '~> 1.19.0'
12
12
  gem 'rubocop-rake', '~> 0.6.0'
13
- gem 'rubocop-rspec', '~> 2.19.0'
13
+ gem 'rubocop-rspec', '~> 2.24.0'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- capistrano-asg-rolling (0.2.1)
4
+ capistrano-asg-rolling (0.4.0)
5
5
  aws-sdk-autoscaling (~> 1, >= 1.67.0)
6
6
  aws-sdk-ec2 (~> 1)
7
7
  capistrano (~> 3)
@@ -10,27 +10,28 @@ PATH
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
- addressable (2.8.4)
13
+ addressable (2.8.5)
14
14
  public_suffix (>= 2.0.2, < 6.0)
15
- airbrussh (1.4.1)
15
+ airbrussh (1.5.0)
16
16
  sshkit (>= 1.6.1, != 1.7.0)
17
17
  ast (2.4.2)
18
18
  aws-eventstream (1.2.0)
19
- aws-partitions (1.748.0)
20
- aws-sdk-autoscaling (1.89.0)
21
- aws-sdk-core (~> 3, >= 3.165.0)
19
+ aws-partitions (1.830.0)
20
+ aws-sdk-autoscaling (1.98.0)
21
+ aws-sdk-core (~> 3, >= 3.184.0)
22
22
  aws-sigv4 (~> 1.1)
23
- aws-sdk-core (3.171.0)
23
+ aws-sdk-core (3.184.0)
24
24
  aws-eventstream (~> 1, >= 1.0.2)
25
25
  aws-partitions (~> 1, >= 1.651.0)
26
26
  aws-sigv4 (~> 1.5)
27
27
  jmespath (~> 1, >= 1.6.1)
28
- aws-sdk-ec2 (1.375.0)
29
- aws-sdk-core (~> 3, >= 3.165.0)
28
+ aws-sdk-ec2 (1.410.0)
29
+ aws-sdk-core (~> 3, >= 3.184.0)
30
30
  aws-sigv4 (~> 1.1)
31
- aws-sigv4 (1.5.2)
31
+ aws-sigv4 (1.6.0)
32
32
  aws-eventstream (~> 1, >= 1.0.2)
33
- capistrano (3.17.2)
33
+ base64 (0.1.1)
34
+ capistrano (3.17.3)
34
35
  airbrussh (>= 1.0.0)
35
36
  i18n
36
37
  rake (>= 10.0.0)
@@ -40,62 +41,70 @@ GEM
40
41
  rexml
41
42
  diff-lcs (1.5.0)
42
43
  hashdiff (1.0.1)
43
- i18n (1.12.0)
44
+ i18n (1.14.1)
44
45
  concurrent-ruby (~> 1.0)
45
46
  jmespath (1.6.2)
46
47
  json (2.6.3)
48
+ language_server-protocol (3.17.0.3)
47
49
  net-scp (4.0.0)
48
50
  net-ssh (>= 2.6.5, < 8.0.0)
49
- net-ssh (7.1.0)
50
- parallel (1.22.1)
51
- parser (3.2.2.0)
51
+ net-ssh (7.2.0)
52
+ parallel (1.23.0)
53
+ parser (3.2.2.3)
52
54
  ast (~> 2.4.1)
53
- public_suffix (5.0.1)
55
+ racc
56
+ public_suffix (5.0.3)
57
+ racc (1.7.1)
54
58
  rainbow (3.1.1)
55
59
  rake (13.0.6)
56
- regexp_parser (2.8.0)
57
- rexml (3.2.5)
60
+ regexp_parser (2.8.1)
61
+ rexml (3.2.6)
58
62
  rspec (3.12.0)
59
63
  rspec-core (~> 3.12.0)
60
64
  rspec-expectations (~> 3.12.0)
61
65
  rspec-mocks (~> 3.12.0)
62
- rspec-core (3.12.1)
66
+ rspec-core (3.12.2)
63
67
  rspec-support (~> 3.12.0)
64
- rspec-expectations (3.12.2)
68
+ rspec-expectations (3.12.3)
65
69
  diff-lcs (>= 1.2.0, < 2.0)
66
70
  rspec-support (~> 3.12.0)
67
- rspec-mocks (3.12.5)
71
+ rspec-mocks (3.12.6)
68
72
  diff-lcs (>= 1.2.0, < 2.0)
69
73
  rspec-support (~> 3.12.0)
70
- rspec-support (3.12.0)
71
- rubocop (1.50.2)
74
+ rspec-support (3.12.1)
75
+ rubocop (1.56.4)
76
+ base64 (~> 0.1.1)
72
77
  json (~> 2.3)
78
+ language_server-protocol (>= 3.17.0)
73
79
  parallel (~> 1.10)
74
- parser (>= 3.2.0.0)
80
+ parser (>= 3.2.2.3)
75
81
  rainbow (>= 2.2.2, < 4.0)
76
82
  regexp_parser (>= 1.8, < 3.0)
77
83
  rexml (>= 3.2.5, < 4.0)
78
- rubocop-ast (>= 1.28.0, < 2.0)
84
+ rubocop-ast (>= 1.28.1, < 2.0)
79
85
  ruby-progressbar (~> 1.7)
80
86
  unicode-display_width (>= 2.4.0, < 3.0)
81
- rubocop-ast (1.28.0)
87
+ rubocop-ast (1.29.0)
82
88
  parser (>= 3.2.1.0)
83
- rubocop-capybara (2.17.1)
89
+ rubocop-capybara (2.19.0)
84
90
  rubocop (~> 1.41)
85
- rubocop-performance (1.17.1)
91
+ rubocop-factory_bot (2.24.0)
92
+ rubocop (~> 1.33)
93
+ rubocop-performance (1.19.1)
86
94
  rubocop (>= 1.7.0, < 2.0)
87
95
  rubocop-ast (>= 0.4.0)
88
96
  rubocop-rake (0.6.0)
89
97
  rubocop (~> 1.0)
90
- rubocop-rspec (2.19.0)
98
+ rubocop-rspec (2.24.1)
91
99
  rubocop (~> 1.33)
92
100
  rubocop-capybara (~> 2.17)
101
+ rubocop-factory_bot (~> 2.22)
93
102
  ruby-progressbar (1.13.0)
94
- sshkit (1.21.4)
103
+ sshkit (1.21.5)
95
104
  net-scp (>= 1.1.2)
96
105
  net-ssh (>= 2.8.0)
97
106
  unicode-display_width (2.4.2)
98
- webmock (3.18.1)
107
+ webmock (3.19.1)
99
108
  addressable (>= 2.8.0)
100
109
  crack (>= 0.3.2)
101
110
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -108,10 +117,10 @@ DEPENDENCIES
108
117
  capistrano-asg-rolling!
109
118
  rake (~> 13.0)
110
119
  rspec (~> 3.0)
111
- rubocop (~> 1.50.2)
112
- rubocop-performance (~> 1.17.0)
120
+ rubocop (~> 1.56.3)
121
+ rubocop-performance (~> 1.19.0)
113
122
  rubocop-rake (~> 0.6.0)
114
- rubocop-rspec (~> 2.19.0)
123
+ rubocop-rspec (~> 2.24.0)
115
124
  webmock (~> 3.11)
116
125
 
117
126
  BUNDLED WITH
data/README.md CHANGED
@@ -101,6 +101,14 @@ When launching an Instance, you can override any settings defined in the Launch
101
101
  set :asg_rolling_instance_overrides, { instance_type: 'c5.large' }
102
102
  ```
103
103
 
104
+ You can make Capistrano wait until the instances in the autoscaling group have completed refreshing with:
105
+
106
+ ```ruby
107
+ # config/deploy.rb
108
+ set :asg_wait_for_instance_refresh, true
109
+ set :asg_instance_refresh_polling_interval, 30 # default
110
+ ```
111
+
104
112
  ## Usage
105
113
 
106
114
  Specify the Auto Scaling Groups with the keyword `autoscale` instead of using the `server` keyword in Capistrano's stage configuration. Provide the name of the Auto Scaling Group and any properties you want to pass to the server:
@@ -125,11 +133,23 @@ autoscale 'app-autoscale-group', rolling: true # default: use rolling deploym
125
133
  autoscale 'web-autoscale-group', rolling: false # override: use normal deployment
126
134
  ```
127
135
 
136
+ ### Deploy with a custom percentage of minimum healthy instances during the instance refresh
137
+
138
+ The instance refresh is triggered by default with a requirement of 100% minimum healthy instances. ie. One instance is replaced at a time, and must be healthy and in-service before the next is replaced. This can mean that instance refreshes take a long time to complete, especially with larger numbers of instances with large warmup values. Reducing this value allows more instances to be terminated and new instances to be brought up at once during the instance refresh. eg. a value of 0 would terminate all instances in the autoscaling group and replace them at once.
139
+
140
+ You can configure the minimum healthy percentage per autoscaling group using the `healthy_percentage` option:
141
+
142
+ ```ruby
143
+ # config/deploy/<stage>.rb
144
+ autoscale 'app-autoscale-group', user: 'deployer' # default: use standard deployment with 100% minimum healthy instances
145
+ autoscale 'web-autoscale-group', user: 'deployer', healthy_percentage: 75 # override: allow 25% of instances to be terminated and replaced at once
146
+ ```
147
+
128
148
  ### Custom stage
129
149
 
130
150
  The rolling configuration of the stage has a side-effect: any Capistrano tasks you run, will also launch instances per Auto Scaling Group.
131
151
 
132
- For example the command: `cap production rails:console`, will launch a new instance and run `rails:console` and that instance. While that can be useful, you often just want to run the task on the primary server. A solution is to create two stages with different rolling configurations, for example:
152
+ For example the command: `cap production rails:console`, will launch a new instance and run `rails:console` on that instance. While that can be useful, you often just want to run the task on the primary server. A solution is to create two stages with different rolling configurations, for example:
133
153
 
134
154
  ```ruby
135
155
  # config/deploy/production.rb
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
27
  spec.require_paths = ['lib']
28
28
 
29
- spec.required_ruby_version = '>= 2.6.0'
29
+ spec.required_ruby_version = '>= 2.7.0'
30
30
 
31
31
  spec.add_development_dependency 'bundler', '~> 2.0'
32
32
  spec.add_development_dependency 'rake', '~> 13.0'
@@ -60,9 +60,9 @@ module Capistrano
60
60
  end
61
61
 
62
62
  def snapshots
63
- @snapshots ||= aws_ec2_image.block_device_mappings.map do |mapping|
63
+ @snapshots ||= aws_ec2_image.block_device_mappings.filter_map do |mapping|
64
64
  Snapshot.new(mapping.ebs.snapshot_id) if mapping.ebs
65
- end.compact
65
+ end
66
66
  end
67
67
 
68
68
  def tags
@@ -12,7 +12,9 @@ module Capistrano
12
12
  LIFECYCLE_STATE_IN_SERVICE = 'InService'
13
13
  LIFECYCLE_STATE_STANDBY = 'Standby'
14
14
 
15
- attr_reader :name, :properties
15
+ COMPLETED_REFRESH_STATUSES = %w[Successful Failed Cancelled RollbackSuccessful RollbackFailed].freeze
16
+
17
+ attr_reader :name, :properties, :refresh_id
16
18
 
17
19
  def initialize(name, properties = {})
18
20
  @name = name
@@ -40,8 +42,12 @@ module Capistrano
40
42
  aws_autoscaling_group.health_check_grace_period
41
43
  end
42
44
 
45
+ def healthy_percentage
46
+ properties.fetch(:healthy_percentage, 100)
47
+ end
48
+
43
49
  def start_instance_refresh(launch_template)
44
- aws_autoscaling_client.start_instance_refresh(
50
+ @refresh_id = aws_autoscaling_client.start_instance_refresh(
45
51
  auto_scaling_group_name: name,
46
52
  strategy: 'Rolling',
47
53
  desired_configuration: {
@@ -52,14 +58,29 @@ module Capistrano
52
58
  },
53
59
  preferences: {
54
60
  instance_warmup: instance_warmup_time,
55
- min_healthy_percentage: 100,
61
+ min_healthy_percentage: healthy_percentage,
56
62
  skip_matching: true
57
63
  }
58
- )
64
+ ).instance_refresh_id
59
65
  rescue Aws::AutoScaling::Errors::InstanceRefreshInProgress => e
60
66
  raise Capistrano::ASG::Rolling::InstanceRefreshFailed, e
61
67
  end
62
68
 
69
+ InstanceRefreshStatus = Struct.new(:status, :percentage_complete) do
70
+ def completed?
71
+ COMPLETED_REFRESH_STATUSES.include?(status)
72
+ end
73
+ end
74
+
75
+ def latest_instance_refresh
76
+ instance_refresh = most_recent_instance_refresh
77
+ status = instance_refresh&.dig(:status)
78
+ percentage_complete = instance_refresh&.dig(:percentage_complete)
79
+ return nil if status.nil?
80
+
81
+ InstanceRefreshStatus.new(status, percentage_complete)
82
+ end
83
+
63
84
  # Returns instances with lifecycle state "InService" for this Auto Scaling Group.
64
85
  def instances
65
86
  instance_ids = aws_autoscaling_group.instances.select { |i| i.lifecycle_state == LIFECYCLE_STATE_IN_SERVICE }.map(&:instance_id)
@@ -102,6 +123,16 @@ module Capistrano
102
123
 
103
124
  private
104
125
 
126
+ def most_recent_instance_refresh
127
+ parameters = {
128
+ auto_scaling_group_name: name,
129
+ max_records: 1
130
+ }
131
+ parameters[:instance_refresh_ids] = [@refresh_id] if @refresh_id
132
+ refresh = aws_autoscaling_client.describe_instance_refreshes(parameters).to_h
133
+ refresh[:instance_refreshes].first
134
+ end
135
+
105
136
  def aws_autoscaling_group
106
137
  @aws_autoscaling_group ||= ::Aws::AutoScaling::AutoScalingGroup.new(name: name, client: aws_autoscaling_client)
107
138
  end
@@ -71,6 +71,14 @@ module Capistrano
71
71
  def rolling_update?
72
72
  fetch(:asg_rolling_update)
73
73
  end
74
+
75
+ def wait_for_instance_refresh?
76
+ fetch(:asg_wait_for_instance_refresh, false)
77
+ end
78
+
79
+ def instance_refresh_polling_interval
80
+ fetch(:asg_instance_refresh_polling_interval, 30)
81
+ end
74
82
  end
75
83
  end
76
84
  end
@@ -15,6 +15,16 @@ module Capistrano
15
15
 
16
16
  class InstanceRefreshFailed < Capistrano::ASG::Rolling::Exception
17
17
  end
18
+
19
+ # Exception when instance terminate fails.
20
+ class InstanceTerminateFailed < Capistrano::ASG::Rolling::Exception
21
+ attr_reader :instance
22
+
23
+ def initialize(instance, exception)
24
+ @instance = instance
25
+ super(exception)
26
+ end
27
+ end
18
28
  end
19
29
  end
20
30
  end
@@ -91,6 +91,8 @@ module Capistrano
91
91
  aws_ec2_client.wait_until(:instance_terminated, instance_ids: [id]) if wait
92
92
 
93
93
  @terminated = true
94
+ rescue Aws::EC2::Errors::ServiceError => e
95
+ raise Capistrano::ASG::Rolling::InstanceTerminateFailed.new(self, e)
94
96
  end
95
97
 
96
98
  def create_ami(name: nil, description: nil, tags: nil)
@@ -13,6 +13,10 @@ module Capistrano
13
13
  $stdout.puts format_text(text)
14
14
  end
15
15
 
16
+ def warning(text)
17
+ $stdout.puts format_text("WARNING: #{text}")
18
+ end
19
+
16
20
  def error(text)
17
21
  $stderr.puts format_text(text, color: :red) # rubocop:disable Style/StderrPuts
18
22
  end
@@ -28,6 +28,7 @@ module Capistrano
28
28
 
29
29
  after 'rolling:update', 'rolling:cleanup'
30
30
  after 'rolling:create_ami', 'rolling:cleanup'
31
+ after 'rolling:update', 'rolling:instance_refresh_status'
31
32
 
32
33
  # Register an exit hook to do some cleanup when Capistrano
33
34
  # terminates without calling our after cleanup hook.
@@ -52,6 +53,8 @@ module Capistrano
52
53
 
53
54
  logger.info 'Terminating instance(s)...'
54
55
  instances.terminate
56
+ rescue Capistrano::ASG::Rolling::InstanceTerminateFailed => e
57
+ logger.warning "Failed to terminate Instance **#{e.instance.id}**: #{e.message}"
55
58
  end
56
59
  end
57
60
  end
@@ -3,7 +3,7 @@
3
3
  module Capistrano
4
4
  module ASG
5
5
  module Rolling
6
- VERSION = '0.2.1'
6
+ VERSION = '0.4.0'
7
7
  end
8
8
  end
9
9
  end
@@ -21,7 +21,7 @@ namespace :rolling do
21
21
  else
22
22
  logger.info "Auto Scaling Group: **#{group.name}**, standard deployment strategy."
23
23
 
24
- group.instances.each_with_index do |instance, index| # rubocop:disable Lint/ShadowingOuterLocalVariable
24
+ group.instances.each_with_index do |instance, index|
25
25
  if index.zero? && group.properties.key?(:primary_roles)
26
26
  server_properties = group.properties.dup
27
27
  server_properties[:roles] = server_properties.delete(:primary_roles)
@@ -86,7 +86,7 @@ namespace :rolling do
86
86
  deleted = deleted_amis.include?(ami)
87
87
 
88
88
  if !exists && !deleted
89
- logger.info("WARNING: AMI **#{ami.id}** does not exist for Launch Template **#{version.name}** version **#{version.version}**.")
89
+ logger.warning("AMI **#{ami.id}** does not exist for Launch Template **#{version.name}** version **#{version.version}**.")
90
90
  next
91
91
  end
92
92
 
@@ -109,7 +109,11 @@ namespace :rolling do
109
109
  instances = config.instances.auto_terminate
110
110
  if instances.any?
111
111
  logger.info 'Terminating instance(s)...'
112
- instances.terminate
112
+ begin
113
+ instances.terminate
114
+ rescue Capistrano::ASG::Rolling::InstanceTerminateFailed => e
115
+ logger.warning "Failed to terminate Instance **#{e.instance.id}**: #{e.message}"
116
+ end
113
117
  end
114
118
  end
115
119
 
@@ -168,4 +172,30 @@ namespace :rolling do
168
172
  end
169
173
  end
170
174
  end
175
+
176
+ desc 'Get status of instance refresh'
177
+ task :instance_refresh_status do
178
+ return unless config.wait_for_instance_refresh?
179
+
180
+ groups = config.autoscale_groups.to_h { |group| [group.name, group] }
181
+
182
+ while groups.any?
183
+ groups.each do |name, group|
184
+ refresh = group.latest_instance_refresh
185
+ if refresh.nil? || refresh.completed?
186
+ logger.info "Auto Scaling Group: **#{name}**, completed with status '#{refresh.status}'." if refresh.completed?
187
+ groups.delete(name)
188
+ elsif !refresh.percentage_complete.nil?
189
+ logger.info "Auto Scaling Group: **#{name}**, #{refresh.percentage_complete}% completed, status '#{refresh.status}'."
190
+ else
191
+ logger.info "Auto Scaling Group: **#{name}**, status '#{refresh.status}'."
192
+ end
193
+ end
194
+ next if groups.empty?
195
+
196
+ wait_for = config.instance_refresh_polling_interval
197
+ logger.info "Instance refresh(es) not completed, waiting #{wait_for} seconds..."
198
+ sleep wait_for
199
+ end
200
+ end
171
201
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capistrano-asg-rolling
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kentaa
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-04-17 00:00:00.000000000 Z
11
+ date: 2023-09-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -180,7 +180,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
180
180
  requirements:
181
181
  - - ">="
182
182
  - !ruby/object:Gem::Version
183
- version: 2.6.0
183
+ version: 2.7.0
184
184
  required_rubygems_version: !ruby/object:Gem::Requirement
185
185
  requirements:
186
186
  - - ">="