capistrano-asg-rolling 0.7.0 → 0.9.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
  SHA256:
3
- metadata.gz: cceded2d7adcb767bc8bb432781055a91af8268303514564a4c1de86ccb2ac2e
4
- data.tar.gz: 8d08579c0a00000bb2457348e9808758d10d74751a08f7071e50fa794fb1ea72
3
+ metadata.gz: f2365c9ab16d5af20fe6502ec5ddb678b71d50516a6e9e514b87a94a6a34f60b
4
+ data.tar.gz: ab344dc87be037b41e0e56e18cdc3532339a0e368983cac5ce8c3707d8b82729
5
5
  SHA512:
6
- metadata.gz: 964743e3ab4ad9813100f3968e45b044d80fc46d6e9141a85b07686b0b4566f8c12bb79f3614fa5d473b358782d3c002d03532f72b60440b666423c4bc5e4947
7
- data.tar.gz: 3366b9a2268485d777a101d1f29335aba073979122347c48e3191f318188ed3ce41eb2f3ec39861b286fa4a0148f9632af23b1d57f93f4e1d857f8f742e929b8
6
+ metadata.gz: fac5e7d23d81518e9e71924b2596be4e630ab1b38d5f57a481c49bfe71abbb5ea704465e22364957998aa00373420d5f7b8ce3805fd0ba05c5ca97b1f373bb3a
7
+ data.tar.gz: 007da3693ba149357ffe55e600d772c52984295e432eb79b65ecb5696e0861572e0776aa36b40ff2adaaee758209337c4406683bc88cb313e064aea1b82d0c93
@@ -9,7 +9,7 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  strategy:
11
11
  matrix:
12
- ruby-version: ['3.1', '3.2', '3.3', '3.4', '4.0']
12
+ ruby-version: ['3.2', '3.3', '3.4', '4.0']
13
13
 
14
14
  steps:
15
15
  - uses: actions/checkout@v2
data/.rubocop.yml CHANGED
@@ -5,7 +5,7 @@ plugins:
5
5
 
6
6
  AllCops:
7
7
  NewCops: enable
8
- TargetRubyVersion: 3.1
8
+ TargetRubyVersion: 3.2
9
9
  DisplayCopNames: true
10
10
  DisplayStyleGuide: true
11
11
 
@@ -39,5 +39,8 @@ RSpec/IndexedLet:
39
39
  RSpec/MultipleExpectations:
40
40
  Max: 5
41
41
 
42
+ RSpec/MultipleMemoizedHelpers:
43
+ Max: 11
44
+
42
45
  Style/GuardClause:
43
46
  Enabled: false
data/CODE_OF_CONDUCT.md CHANGED
@@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
55
55
  ## Enforcement
56
56
 
57
57
  Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
- reported by contacting the project team at peter.postma@kentaa.nl. All
58
+ reported by contacting the project team at tech-arnhem@iraiser.eu. All
59
59
  complaints will be reviewed and investigated and will result in a response that
60
60
  is deemed necessary and appropriate to the circumstances. The project team is
61
61
  obligated to maintain confidentiality with regard to the reporter of an incident.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- capistrano-asg-rolling (0.7.0)
4
+ capistrano-asg-rolling (0.9.0)
5
5
  aws-sdk-autoscaling (~> 1, >= 1.100.0)
6
6
  aws-sdk-ec2 (~> 1)
7
7
  capistrano (~> 3)
@@ -10,17 +10,17 @@ PATH
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
- addressable (2.8.8)
13
+ addressable (2.9.0)
14
14
  public_suffix (>= 2.0.2, < 8.0)
15
- airbrussh (1.6.0)
15
+ airbrussh (1.6.2)
16
16
  sshkit (>= 1.6.1, != 1.7.0)
17
17
  ast (2.4.3)
18
18
  aws-eventstream (1.4.0)
19
- aws-partitions (1.1198.0)
20
- aws-sdk-autoscaling (1.149.0)
21
- aws-sdk-core (~> 3, >= 3.239.1)
19
+ aws-partitions (1.1282.0)
20
+ aws-sdk-autoscaling (1.166.0)
21
+ aws-sdk-core (~> 3, >= 3.254.0)
22
22
  aws-sigv4 (~> 1.5)
23
- aws-sdk-core (3.240.0)
23
+ aws-sdk-core (3.254.1)
24
24
  aws-eventstream (~> 1, >= 1.3.0)
25
25
  aws-partitions (~> 1, >= 1.992.0)
26
26
  aws-sigv4 (~> 1.9)
@@ -28,47 +28,47 @@ GEM
28
28
  bigdecimal
29
29
  jmespath (~> 1, >= 1.6.1)
30
30
  logger
31
- aws-sdk-ec2 (1.587.0)
32
- aws-sdk-core (~> 3, >= 3.239.1)
31
+ aws-sdk-ec2 (1.641.0)
32
+ aws-sdk-core (~> 3, >= 3.254.0)
33
33
  aws-sigv4 (~> 1.5)
34
34
  aws-sigv4 (1.12.1)
35
35
  aws-eventstream (~> 1, >= 1.0.2)
36
36
  base64 (0.3.0)
37
- bigdecimal (4.0.1)
38
- capistrano (3.20.0)
37
+ bigdecimal (4.1.2)
38
+ capistrano (3.20.1)
39
39
  airbrussh (>= 1.0.0)
40
40
  i18n
41
41
  rake (>= 10.0.0)
42
42
  sshkit (>= 1.9.0)
43
- concurrent-ruby (1.3.6)
43
+ concurrent-ruby (1.3.8)
44
44
  crack (1.0.1)
45
45
  bigdecimal
46
46
  rexml
47
47
  diff-lcs (1.6.2)
48
48
  hashdiff (1.2.1)
49
- i18n (1.14.8)
49
+ i18n (1.15.2)
50
50
  concurrent-ruby (~> 1.0)
51
51
  jmespath (1.6.2)
52
- json (2.18.0)
53
- language_server-protocol (3.17.0.5)
52
+ json (2.21.2)
53
+ language_server-protocol (3.17.0.6)
54
54
  lint_roller (1.1.0)
55
55
  logger (1.7.0)
56
56
  net-scp (4.1.0)
57
57
  net-ssh (>= 2.6.5, < 8.0.0)
58
58
  net-sftp (4.0.0)
59
59
  net-ssh (>= 5.0.0, < 8.0.0)
60
- net-ssh (7.3.0)
60
+ net-ssh (7.3.3)
61
61
  ostruct (0.6.3)
62
- parallel (1.27.0)
63
- parser (3.3.10.0)
62
+ parallel (1.28.0)
63
+ parser (3.3.12.0)
64
64
  ast (~> 2.4.1)
65
65
  racc
66
- prism (1.7.0)
67
- public_suffix (6.0.2)
66
+ prism (1.9.0)
67
+ public_suffix (7.0.5)
68
68
  racc (1.8.1)
69
69
  rainbow (3.1.1)
70
- rake (13.3.1)
71
- regexp_parser (2.11.3)
70
+ rake (13.4.2)
71
+ regexp_parser (2.12.0)
72
72
  rexml (3.4.4)
73
73
  rspec (3.13.2)
74
74
  rspec-core (~> 3.13.0)
@@ -79,36 +79,38 @@ GEM
79
79
  rspec-expectations (3.13.5)
80
80
  diff-lcs (>= 1.2.0, < 2.0)
81
81
  rspec-support (~> 3.13.0)
82
- rspec-mocks (3.13.7)
82
+ rspec-mocks (3.13.8)
83
83
  diff-lcs (>= 1.2.0, < 2.0)
84
84
  rspec-support (~> 3.13.0)
85
- rspec-support (3.13.6)
86
- rubocop (1.82.0)
87
- json (~> 2.3)
85
+ rspec-support (3.13.7)
86
+ rubocop (1.90.0)
87
+ json (>= 2.3)
88
88
  language_server-protocol (~> 3.17.0.2)
89
89
  lint_roller (~> 1.1.0)
90
- parallel (~> 1.10)
90
+ parallel (>= 1.10)
91
91
  parser (>= 3.3.0.2)
92
92
  rainbow (>= 2.2.2, < 4.0)
93
93
  regexp_parser (>= 2.9.3, < 3.0)
94
- rubocop-ast (>= 1.48.0, < 2.0)
94
+ rubocop-ast (>= 1.49.0, < 2.0)
95
95
  ruby-progressbar (~> 1.7)
96
96
  unicode-display_width (>= 2.4.0, < 4.0)
97
- rubocop-ast (1.48.0)
97
+ rubocop-ast (1.50.0)
98
98
  parser (>= 3.3.7.2)
99
- prism (~> 1.4)
100
- rubocop-performance (1.26.1)
99
+ prism (~> 1.7)
100
+ rubocop-performance (1.27.0)
101
101
  lint_roller (~> 1.1)
102
- rubocop (>= 1.75.0, < 2.0)
102
+ rubocop (>= 1.89.0, < 2.0)
103
103
  rubocop-ast (>= 1.47.1, < 2.0)
104
104
  rubocop-rake (0.7.1)
105
105
  lint_roller (~> 1.1)
106
106
  rubocop (>= 1.72.1)
107
- rubocop-rspec (3.8.0)
107
+ rubocop-rspec (3.10.2)
108
108
  lint_roller (~> 1.1)
109
- rubocop (~> 1.81)
109
+ regexp_parser (>= 2.0)
110
+ rubocop (~> 1.86, >= 1.86.2)
110
111
  ruby-progressbar (1.13.0)
111
- sshkit (1.25.0)
112
+ simplecov (1.1.1)
113
+ sshkit (1.25.1)
112
114
  base64
113
115
  logger
114
116
  net-scp (>= 1.1.2)
@@ -118,7 +120,7 @@ GEM
118
120
  unicode-display_width (3.2.0)
119
121
  unicode-emoji (~> 4.1)
120
122
  unicode-emoji (4.2.0)
121
- webmock (3.26.1)
123
+ webmock (3.26.3)
122
124
  addressable (>= 2.8.0)
123
125
  crack (>= 0.3.2)
124
126
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -135,6 +137,7 @@ DEPENDENCIES
135
137
  rubocop-performance (~> 1.20)
136
138
  rubocop-rake (~> 0.6)
137
139
  rubocop-rspec (~> 3.0)
140
+ simplecov (~> 1.0)
138
141
  webmock (~> 3.11)
139
142
 
140
143
  BUNDLED WITH
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2021 Kentaa BV
3
+ Copyright (c) 2025 Kentaa BV
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -6,15 +6,15 @@
6
6
 
7
7
  Capistrano plugin for performing rolling updates to AWS Auto Scaling Groups using the [instance refresh feature](https://docs.aws.amazon.com/autoscaling/ec2/userguide/asg-instance-refresh.html).
8
8
 
9
- Instead of deploying to live servers, capistrano-asg-rolling will create a temporary instance for deployment and then trigger an instance refresh to perform a rolling update of the Auto Scaling Group(s). In more detail, during deployment it will:
9
+ Instead of deploying to live servers, capistrano-asg-rolling will create temporary instances for deployment and then trigger an instance refresh to perform a rolling update of the Auto Scaling Group(s). In more detail, during deployment it will:
10
10
 
11
- - Launch an instance from the AMI defined in the Launch Template of the Auto Scaling Group(s).
11
+ - Launch an instance for each Auto Scaling Group, from the AMI defined in its Launch Template.
12
12
  - Deploy your application to the launched instances.
13
13
  - After deployment, stop the instances and create an AMI for each instance.
14
14
  - Create new Launch Template versions for the new AMIs.
15
15
  - Trigger Instance Refresh on the Auto Scaling Group(s) to perform a rolling update.
16
16
  - Delete any outdated Launch Template versions, AMIs and snapshots created by previous deployments.
17
- - Terminate the no longer needed instances.
17
+ - Terminate the temporary instances.
18
18
 
19
19
  ## Important
20
20
 
@@ -25,13 +25,13 @@ https://docs.aws.amazon.com/autoscaling/ec2/userguide/instance-refresh-overview.
25
25
 
26
26
  ### Launch Templates
27
27
 
28
- This gem depends on Auto Scaling Groups with Launch Templates. Using an Auto Scaling Group with a Launch Configuration is not supported, and will raise an `Capistrano::ASG::Rolling::NoLaunchTemplate`.
28
+ This gem depends on Auto Scaling Groups with Launch Templates. Using an Auto Scaling Group with a Launch Configuration is not supported, and will raise a `Capistrano::ASG::Rolling::NoLaunchTemplate` error.
29
29
 
30
- Instance refresh uses the desired configuration to update the Launch Template version of the Auto Scaling Group after a succesful deployment. Setting the Launch Template version to `Latest` on the Auto Scaling Group is not needed.
30
+ Instance refresh uses the desired configuration to update the Launch Template version of the Auto Scaling Group after a successful deployment. Setting the Launch Template version to `Latest` on the Auto Scaling Group is not needed.
31
31
 
32
32
  ### Experimental
33
33
 
34
- Please note that this gem works well for our configuration / use case, but it might not fit yours. Any feedback using GitHub issues / pull requests, is much appreciated.
34
+ Please note that this gem works well for our configuration / use case, but it might not fit yours. Any feedback via GitHub issues / pull requests is much appreciated.
35
35
 
36
36
  ## Installation
37
37
 
@@ -59,60 +59,96 @@ install_plugin Capistrano::ASG::Rolling::Plugin
59
59
 
60
60
  ## Configuration
61
61
 
62
- Below are the Capistrano configuration options:
62
+ Below you can find all configurable settings for this gem.
63
63
 
64
- Setup AWS credentials:
64
+ ### AWS
65
+
66
+ Set up the AWS credentials and region:
65
67
 
66
68
  ```ruby
67
69
  # config/deploy.rb
68
70
  set :aws_access_key_id, ENV['AWS_ACCESS_KEY_ID']
69
71
  set :aws_secret_access_key, ENV['AWS_SECRET_ACCESS_KEY']
72
+ set :aws_session_token, ENV['AWS_SESSION_TOKEN']
70
73
  set :aws_region, ENV['AWS_REGION']
71
74
  ```
72
75
 
73
- After deployment, any outdated Launch Template versions, AMIs and snapshots will be deleted. By default, the number of `keep_releases` will be kept. Change this with:
76
+ The AWS clients are configured with `adaptive` retry mode and a retry limit of 10 by default, so transient throttling (`Aws::AutoScaling::Errors::Throttling: Rate exceeded`) does not abort a deployment. You can tune both:
74
77
 
75
78
  ```ruby
76
79
  # config/deploy.rb
77
- set :asg_rolling_keep_versions, 10
80
+ set :asg_aws_retry_mode, 'adaptive' # default; one of 'legacy', 'standard', 'adaptive'
81
+ set :asg_aws_retry_limit, 10 # default
78
82
  ```
79
83
 
80
- Servers are added using their private IP address by default. Set to false to use the public IP address instead:
84
+ ### Launched instances
85
+
86
+ Launched instances are added to Capistrano as servers using their private IP address by default. Set this option to false to use the public IP address instead:
81
87
 
82
88
  ```ruby
83
89
  # config/deploy.rb
84
90
  set :asg_rolling_use_private_ip_address, false
85
91
  ```
86
92
 
87
- Verbose logging is enabled by default, set to false for less verbose logging:
93
+ When launching an instance, you can override any settings defined in the Launch Template with:
88
94
 
89
95
  ```ruby
90
96
  # config/deploy.rb
91
- set :asg_rolling_verbose, false
97
+ set :asg_rolling_instance_overrides, { instance_type: 'c5.large' }
92
98
  ```
93
99
 
94
- When launching an Instance, you can override any settings defined in the Launch Template with:
100
+ ### AMI
101
+
102
+ After creating an AMI, the gem waits until it becomes available before triggering an instance refresh. The defaults match the AWS SDK defaults (~10 minutes), but for larger root volumes (e.g. resizing 24 GB → 32 GB) the AMI can take longer to become available. Override the waiter via:
95
103
 
96
104
  ```ruby
97
105
  # config/deploy.rb
98
- set :asg_rolling_instance_overrides, { instance_type: 'c5.large' }
106
+ set :asg_ami_wait_delay, 15 # seconds between polls (default: 15)
107
+ set :asg_ami_wait_max_attempts, 40 # max polls (default: 40, ≈ 10 minutes)
99
108
  ```
100
109
 
101
- You can make Capistrano wait until the instances in the autoscaling group have completed refreshing with:
110
+ ### Instance refresh
111
+
112
+ You can make Capistrano wait until the instance refresh on the Auto Scaling Group(s) has completed:
102
113
 
103
114
  ```ruby
104
115
  # config/deploy.rb
105
116
  set :asg_wait_for_instance_refresh, true
106
- set :asg_instance_refresh_polling_interval, 30 # default
117
+ set :asg_instance_refresh_polling_interval, 30 # seconds (default: 30)
107
118
  ```
108
119
 
109
- Enable or disable auto-rollback on instance refreshes (default: false):
120
+ Enable auto-rollback to have the Auto Scaling Group roll back to its previous configuration when the instance refresh fails (default: false):
110
121
 
111
122
  ```ruby
112
123
  # config/deploy.rb
113
124
  set :asg_instance_refresh_auto_rollback, true
114
125
  ```
115
126
 
127
+ ### Cleanup
128
+
129
+ After deployment, any outdated Launch Template versions, AMIs and snapshots will be deleted. By default, the same number of versions as Capistrano's `keep_releases` will be kept. Change this with:
130
+
131
+ ```ruby
132
+ # config/deploy.rb
133
+ set :asg_rolling_keep_versions, 10
134
+ ```
135
+
136
+ ### Logging
137
+
138
+ Verbose logging is enabled by default; set this option to false for less verbose logging:
139
+
140
+ ```ruby
141
+ # config/deploy.rb
142
+ set :asg_rolling_log_verbose, false
143
+ ```
144
+
145
+ By default, the log output does not contain any timestamps; set the option below to true to enable timestamps:
146
+
147
+ ```ruby
148
+ # config/deploy.rb
149
+ set :asg_rolling_log_timestamp, true
150
+ ```
151
+
116
152
  ## Usage
117
153
 
118
154
  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:
@@ -129,7 +165,7 @@ Now start a deployment with `cap <stage> deploy` and enjoy.
129
165
 
130
166
  There might be cases where you just want to deploy your code to the servers in the Auto Scaling Group(s) without a rolling update.
131
167
 
132
- You can configure rolling updates per autoscaling group by using the `rolling` option:
168
+ You can configure rolling updates per Auto Scaling Group by using the `rolling` option:
133
169
 
134
170
  ```ruby
135
171
  # config/deploy/<stage>.rb
@@ -137,28 +173,28 @@ autoscale 'app-autoscale-group', rolling: true # default: use rolling deploym
137
173
  autoscale 'web-autoscale-group', rolling: false # override: use normal deployment
138
174
  ```
139
175
 
140
- ### Deploy with custom percentage of minimum/maximum healthy instances during the instance refresh
176
+ ### Deploy with custom minimum/maximum healthy percentages
141
177
 
142
178
  The instance refresh is triggered without specifying a value for the minimum / maximum healthy percentages. This means that either the default
143
179
  values will be used (minimum: 90%, maximum: 100%) or the percentages set in the instance maintenance policy for the Auto Scaling Group.
144
- You can tune both the minimum and maximum values to have more control about the desired capacity that must be healthy to proceed with replacing instances.
180
+ You can tune both the minimum and maximum values to have more control over the desired capacity that must be healthy to proceed with replacing instances.
145
181
 
146
- For example: reducing the minimum healthy percentage 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.
182
+ For example, reducing the minimum healthy percentage allows more instances to be terminated and replaced at once during the instance refresh; a value of 0 would terminate and replace all instances in the Auto Scaling Group at once.
147
183
 
148
- You can configure the minimum healthy percentage per autoscaling group using the `min_healthy_percentage` option, and the maximum healthy percentage using the `max_healthy_percentage` option. Please note that if you specify `max_healthy_percentage`, you must also specify `min_healthy_percentage`.
184
+ You can configure the minimum healthy percentage per Auto Scaling Group using the `min_healthy_percentage` option, and the maximum healthy percentage using the `max_healthy_percentage` option. Please note that if you specify `max_healthy_percentage`, you must also specify `min_healthy_percentage`.
149
185
 
150
186
  ```ruby
151
187
  # config/deploy/<stage>.rb
152
- autoscale 'app-autoscale-group', # use default percentages or percentages set in the instance maintenance policy
188
+ autoscale 'app-autoscale-group' # use default percentages or percentages set in the instance maintenance policy
153
189
  autoscale 'web-autoscale-group', min_healthy_percentage: 75 # allow 25% of instances to be terminated and replaced at once
154
190
  autoscale 'web-autoscale-group', min_healthy_percentage: 100, max_healthy_percentage: 110 # allow for 10% above desired capacity during instance refresh
155
191
  ```
156
192
 
157
193
  ### Custom stage
158
194
 
159
- The rolling configuration of the stage has a side-effect: any Capistrano tasks you run, will also launch instances per Auto Scaling Group.
195
+ The rolling configuration of the stage has a side-effect: any Capistrano task you run will also launch instances per Auto Scaling Group.
160
196
 
161
- 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:
197
+ 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:
162
198
 
163
199
  ```ruby
164
200
  # config/deploy/production.rb
@@ -183,7 +219,7 @@ With these two stages, you can run any tasks with `cap production <task name>` a
183
219
 
184
220
  ### Tags
185
221
 
186
- During deployment, the following tags will be added to the AMI and snapshot containing information about the current application, stage and deployment:
222
+ During deployment, the following tags will be added to the AMI and snapshot, containing information about the current application, stage and deployment:
187
223
  - `capistrano-asg-rolling:application`
188
224
  - `capistrano-asg-rolling:stage`
189
225
  - `capistrano-asg-rolling:deployment-branch`
@@ -192,7 +228,7 @@ During deployment, the following tags will be added to the AMI and snapshot cont
192
228
  - `capistrano-asg-rolling:deployment-user`
193
229
 
194
230
  In addition to that, the tag `capistrano-asg-rolling:gem-version` will be added with the value of the current gem version.
195
- This tag is also used to determine if the AMI was created by this gem, and can be deleted automatically.
231
+ This tag is also used to determine whether the AMI was created by this gem and can therefore be deleted automatically.
196
232
 
197
233
  You can add custom tag(s) to the AMI and snapshot by setting the property `asg_rolling_ami_tags`, for example:
198
234
 
@@ -205,7 +241,7 @@ set :asg_rolling_ami_tags, { 'Application' => 'My Application', 'Environment' =>
205
241
 
206
242
  ### Test deployment
207
243
 
208
- Do a test deployment: run the deploy task, but do not trigger the update ASG task and do not automatically terminate instances.
244
+ Do a test deployment: run the deploy task, but do not trigger an instance refresh and do not automatically terminate the launched instances.
209
245
 
210
246
  ```bash
211
247
  cap <stage> rolling:deploy_test
@@ -213,8 +249,8 @@ cap <stage> rolling:deploy_test
213
249
 
214
250
  ### Launch instances
215
251
 
216
- Just launch instance(s) defined in the Auto Scale Group(s) launch template.
217
- This instance is not attached to the Auto Scale Group and needs to be terminated manually.
252
+ Launch instance(s) from the Launch Template of the Auto Scaling Group(s).
253
+ These instances are not attached to the Auto Scaling Group and need to be terminated manually.
218
254
 
219
255
  ```bash
220
256
  cap <stage> rolling:launch_instances
@@ -222,8 +258,8 @@ cap <stage> rolling:launch_instances
222
258
 
223
259
  ### Create AMI of instance in ASG
224
260
 
225
- Pick an instance in the Auto Scale Group(s), put it into standby and stop it.
226
- Then create an AMI and a new launch template. Then start the instance and put it into service again.
261
+ Pick an instance in the Auto Scaling Group(s), put it into standby and stop it.
262
+ Then create an AMI, start the instance and put it back into service, and create a new Launch Template version with the new AMI.
227
263
 
228
264
  ```bash
229
265
  cap <stage> rolling:create_ami
@@ -231,7 +267,7 @@ cap <stage> rolling:create_ami
231
267
 
232
268
  ## Filtering
233
269
 
234
- You can filter any command to run on a specific Auto Scale Group by using the parameter `asg_name`.
270
+ You can filter any command to run on a specific Auto Scaling Group by using the parameter `asg_name`.
235
271
 
236
272
  For example, the command:
237
273
 
@@ -239,7 +275,7 @@ For example, the command:
239
275
  cap <stage> deploy asg_name=app-autoscale-group
240
276
  ```
241
277
 
242
- Will do a deployment only on the Auto Scale Group with the name `app-autoscale-group`.
278
+ This will do a deployment only on the Auto Scaling Group named `app-autoscale-group`.
243
279
 
244
280
  ## IAM policy
245
281
 
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path('lib', __dir__)
4
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
- require 'capistrano/asg/rolling/version'
3
+ require_relative 'lib/capistrano/asg/rolling/version'
6
4
 
7
5
  Gem::Specification.new do |spec|
8
6
  spec.name = 'capistrano-asg-rolling'
9
7
  spec.version = Capistrano::ASG::Rolling::VERSION
10
- spec.authors = ['Kentaa']
11
- spec.email = ['developers@kentaa.nl']
8
+ spec.authors = %w[Kentaa iRaiser]
9
+ spec.email = ['tech-arnhem@iraiser.eu']
12
10
 
13
11
  spec.summary = 'Capistrano plugin for performing rolling updates to AWS Auto Scaling Groups using Instance Refresh'
14
12
  spec.homepage = 'https://github.com/KentaaNL/capistrano-asg-rolling'
@@ -26,11 +24,12 @@ Gem::Specification.new do |spec|
26
24
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
25
  spec.require_paths = ['lib']
28
26
 
29
- spec.required_ruby_version = '>= 3.1.0'
27
+ spec.required_ruby_version = '>= 3.2.0'
30
28
 
31
29
  spec.add_development_dependency 'bundler', '~> 2.0'
32
30
  spec.add_development_dependency 'rake', '~> 13.0'
33
31
  spec.add_development_dependency 'rspec', '~> 3.0'
32
+ spec.add_development_dependency 'simplecov', '~> 1.0'
34
33
  spec.add_development_dependency 'webmock', '~> 3.11'
35
34
 
36
35
  spec.add_dependency 'aws-sdk-autoscaling', '~> 1', '>= 1.100.0'
@@ -36,10 +36,17 @@ module Capistrano
36
36
  response = aws_ec2_client.create_image(options)
37
37
 
38
38
  begin
39
- aws_ec2_client.wait_until(:image_available, image_ids: [response.image_id])
40
- rescue Aws::Waiters::Errors::TooManyAttemptsError
39
+ aws_ec2_client.wait_until(:image_available, image_ids: [response.image_id]) do |waiter|
40
+ waiter.delay = Configuration.ami_wait_delay
41
+ waiter.max_attempts = Configuration.ami_wait_max_attempts
42
+ end
43
+ rescue Aws::Waiters::Errors::TooManyAttemptsError => e
41
44
  # When waiting for the AMI takes longer than the default (10 minutes),
42
- # then assume it will eventually succeed and just continue.
45
+ # then assume it will eventually succeed and just continue. Surface a
46
+ # warning so operators can see that the wait did not complete in time
47
+ # (for example after increasing the root volume size of the source
48
+ # instance) before the subsequent Instance Refresh is started.
49
+ Kernel.warn("WARNING: timed out waiting for AMI #{response.image_id} to become available: #{e.message}")
43
50
  end
44
51
 
45
52
  new(response.image_id, instance)
@@ -21,12 +21,6 @@ module Capistrano
21
21
  @name = name
22
22
  @properties = properties
23
23
 
24
- if properties[:healthy_percentage]
25
- properties[:min_healthy_percentage] = properties.delete(:healthy_percentage)
26
-
27
- Kernel.warn('WARNING: the property `healthy_percentage` is deprecated and will be removed in a future release. Please update to `min_healthy_percentage`.')
28
- end
29
-
30
24
  validate_properties!
31
25
  end
32
26
 
@@ -81,7 +75,11 @@ module Capistrano
81
75
  auto_rollback: auto_rollback
82
76
  }.compact
83
77
  ).instance_refresh_id
84
- rescue Aws::AutoScaling::Errors::InstanceRefreshInProgress => e
78
+ rescue Aws::AutoScaling::Errors::ServiceError => e
79
+ # Wrap all AWS Auto Scaling service errors (InstanceRefreshInProgress,
80
+ # ValidationError, throttling, etc.) so the rake task can rescue a
81
+ # single gem-defined error type and continue with the remaining
82
+ # Auto Scaling Group(s).
85
83
  raise Capistrano::ASG::Rolling::StartInstanceRefreshError, e
86
84
  end
87
85
 
@@ -102,6 +100,10 @@ module Capistrano
102
100
  return nil if status.nil?
103
101
 
104
102
  InstanceRefreshStatus.new(status, percentage_complete)
103
+ rescue Aws::AutoScaling::Errors::ServiceError => e
104
+ # Wrap AWS service errors (typically throttling that exceeded the SDK's
105
+ # retry budget) so callers can rescue a gem-defined error type.
106
+ raise Capistrano::ASG::Rolling::InstanceRefreshStatusError, e
105
107
  end
106
108
 
107
109
  # Returns instances with lifecycle state "InService" for this Auto Scaling Group.
@@ -19,8 +19,24 @@ module Capistrano
19
19
  @groups.reject { |group| filtered?(group) }.each(&)
20
20
  end
21
21
 
22
+ def empty?
23
+ none?
24
+ end
25
+
26
+ def rolling
27
+ self.class.new(select(&:rolling?))
28
+ end
29
+
30
+ def standard
31
+ self.class.new(reject(&:rolling?))
32
+ end
33
+
34
+ def with_unique_images
35
+ self.class.new(uniq { |group| group.launch_template.image_id })
36
+ end
37
+
22
38
  def launch_templates
23
- templates = @groups.map(&:launch_template)
39
+ templates = map(&:launch_template)
24
40
  LaunchTemplates.new(templates)
25
41
  end
26
42
 
@@ -6,14 +6,16 @@ require 'aws-sdk-ec2'
6
6
  module Capistrano
7
7
  module ASG
8
8
  module Rolling
9
- # AWS SDK.
9
+ # AWS SDK helpers. Clients are memoized per object and shared across objects on the same thread.
10
10
  module AWS
11
11
  def aws_autoscaling_client
12
- @aws_autoscaling_client ||= ::Aws::AutoScaling::Client.new(aws_options)
12
+ @aws_autoscaling_client ||=
13
+ Thread.current[:aws_autoscaling_client] ||= ::Aws::AutoScaling::Client.new(aws_options)
13
14
  end
14
15
 
15
16
  def aws_ec2_client
16
- @aws_ec2_client ||= ::Aws::EC2::Client.new(aws_options)
17
+ @aws_ec2_client ||=
18
+ Thread.current[:aws_ec2_client] ||= ::Aws::EC2::Client.new(aws_options)
17
19
  end
18
20
 
19
21
  private
@@ -22,6 +24,8 @@ module Capistrano
22
24
  options = {}
23
25
  options[:region] = aws_region if aws_region
24
26
  options[:credentials] = aws_credentials if aws_credentials.set?
27
+ options[:retry_mode] = Configuration.aws_retry_mode
28
+ options[:retry_limit] = Configuration.aws_retry_limit
25
29
  options[:http_wire_trace] = true if ENV['AWS_HTTP_WIRE_TRACE'] == '1'
26
30
  options
27
31
  end
@@ -3,25 +3,39 @@
3
3
  module Capistrano
4
4
  module ASG
5
5
  module Rolling
6
- # Singleton that holds the configuration.
6
+ # Singleton that holds the configuration and global state of the plugin.
7
+ # State is eagerly initialized so accessors are lock-free.
7
8
  module Configuration
8
9
  extend Capistrano::DSL
9
10
 
11
+ # Registered Auto Scaling Groups.
12
+ @autoscale_groups = AutoscaleGroups.new
13
+
14
+ # Launched Instances.
15
+ @instances = Instances.new
16
+
17
+ # Updated Launch Templates.
18
+ @launch_templates = LaunchTemplates.new
19
+
10
20
  module_function
11
21
 
12
- # Registered Auto Scaling Groups.
13
22
  def autoscale_groups
14
- @autoscale_groups ||= AutoscaleGroups.new
23
+ @autoscale_groups
15
24
  end
16
25
 
17
- # Launched Instances.
18
26
  def instances
19
- @instances ||= Instances.new
27
+ @instances
20
28
  end
21
29
 
22
- # Updated Launch Templates.
23
30
  def launch_templates
24
- @launch_templates ||= LaunchTemplates.new
31
+ @launch_templates
32
+ end
33
+
34
+ # Resets all mutable state.
35
+ def reset!
36
+ @autoscale_groups = AutoscaleGroups.new
37
+ @instances = Instances.new
38
+ @launch_templates = LaunchTemplates.new
25
39
  end
26
40
 
27
41
  def aws_access_key_id
@@ -44,10 +58,6 @@ module Capistrano
44
58
  fetch(:asg_rolling_group_name)
45
59
  end
46
60
 
47
- def ssh_options
48
- fetch(:asg_rolling_ssh_options, fetch(:ssh_options))
49
- end
50
-
51
61
  def instance_overrides
52
62
  fetch(:asg_rolling_instance_overrides)
53
63
  end
@@ -60,8 +70,12 @@ module Capistrano
60
70
  fetch(:asg_rolling_keep_versions, fetch(:keep_releases))
61
71
  end
62
72
 
63
- def verbose?
64
- fetch(:asg_rolling_verbose)
73
+ def log_verbose?
74
+ fetch(:asg_rolling_log_verbose)
75
+ end
76
+
77
+ def log_timestamp?
78
+ fetch(:asg_rolling_log_timestamp)
65
79
  end
66
80
 
67
81
  def rolling_update=(value)
@@ -79,6 +93,22 @@ module Capistrano
79
93
  def instance_refresh_polling_interval
80
94
  fetch(:asg_instance_refresh_polling_interval, 30)
81
95
  end
96
+
97
+ def aws_retry_mode
98
+ fetch(:asg_aws_retry_mode, 'adaptive')
99
+ end
100
+
101
+ def aws_retry_limit
102
+ fetch(:asg_aws_retry_limit, 10)
103
+ end
104
+
105
+ def ami_wait_delay
106
+ fetch(:asg_ami_wait_delay, 15)
107
+ end
108
+
109
+ def ami_wait_max_attempts
110
+ fetch(:asg_ami_wait_max_attempts, 40)
111
+ end
82
112
  end
83
113
  end
84
114
  end
@@ -13,9 +13,14 @@ module Capistrano
13
13
  class NoLaunchTemplate < Capistrano::ASG::Rolling::Exception
14
14
  end
15
15
 
16
+ # Exception when starting an instance refresh failed.
16
17
  class StartInstanceRefreshError < Capistrano::ASG::Rolling::Exception
17
18
  end
18
19
 
20
+ # Exception when fetching the instance refresh status failed.
21
+ class InstanceRefreshStatusError < Capistrano::ASG::Rolling::Exception
22
+ end
23
+
19
24
  # Exception when the instance refresh failed on one of the ASGs.
20
25
  class InstanceRefreshFailed < Capistrano::ASG::Rolling::Exception
21
26
  def initialize
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'concurrent/array'
4
+
3
5
  module Capistrano
4
6
  module ASG
5
7
  module Rolling
@@ -8,7 +10,7 @@ module Capistrano
8
10
  include Enumerable
9
11
 
10
12
  def initialize(instances = [])
11
- @instances = instances
13
+ @instances = Concurrent::Array.new(instances)
12
14
  end
13
15
 
14
16
  def <<(instance)
@@ -23,7 +25,7 @@ module Capistrano
23
25
  instances.empty?
24
26
  end
25
27
 
26
- def auto_terminate
28
+ def auto_terminatable
27
29
  self.class.new(select(&:auto_terminate?))
28
30
  end
29
31
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
-
5
3
  module Capistrano
6
4
  module ASG
7
5
  module Rolling
@@ -29,16 +27,20 @@ module Capistrano
29
27
  @templates.empty?
30
28
  end
31
29
 
30
+ # For each AMI, find templates still pointing at the old image it was built
31
+ # from, then create new template versions referencing the new AMI in parallel.
32
+ #
33
+ # Returns `LaunchTemplates` containing only the newly created versions.
32
34
  def update(amis:, description: nil)
33
- updated_templates = []
34
-
35
- amis.each do |ami|
35
+ template_image_pairs = amis.flat_map do |ami|
36
36
  old_image_id = ami.instance.image_id
37
37
  new_image_id = ami.id
38
38
 
39
- with_image(old_image_id).each do |template|
40
- updated_templates << template.create_version(image_id: new_image_id, description: description)
41
- end
39
+ with_image(old_image_id).map { |template| [template, new_image_id] }
40
+ end
41
+
42
+ updated_templates = Parallel.run(template_image_pairs) do |template, new_image_id|
43
+ template.create_version(image_id: new_image_id, description: description)
42
44
  end
43
45
 
44
46
  self.class.new(updated_templates)
@@ -3,9 +3,10 @@
3
3
  module Capistrano
4
4
  module ASG
5
5
  module Rolling
6
- # Logging support.
6
+ # Colorized stdout/stderr output with optional timestamps and **bold** formatting.
7
7
  class Logger
8
- def initialize(verbose: false)
8
+ def initialize(timestamp: false, verbose: false)
9
+ @timestamp = timestamp
9
10
  @verbose = verbose
10
11
  end
11
12
 
@@ -28,6 +29,7 @@ module Capistrano
28
29
  private
29
30
 
30
31
  def format_text(text, color: nil)
32
+ text = "[#{current_time}] #{text}" if @timestamp
31
33
  text = colorize_text(text, color) if color
32
34
  text.gsub(/\*\*(.+?)\*\*/, bold_text('\\1'))
33
35
  end
@@ -43,6 +45,10 @@ module Capistrano
43
45
  def _color
44
46
  @_color ||= SSHKit::Color.new($stdout)
45
47
  end
48
+
49
+ def current_time
50
+ Time.now.strftime('%H:%M:%S')
51
+ end
46
52
  end
47
53
  end
48
54
  end
@@ -9,18 +9,34 @@ module Capistrano
9
9
  module Parallel
10
10
  module_function
11
11
 
12
+ # Runs the given block once per element of `work`, in parallel.
13
+ #
14
+ # All threads are joined before this method returns, so a failure in one
15
+ # block does not abandon the other in-flight threads. If any block
16
+ # raises, the first error is re-raised after all threads have completed;
17
+ # additional errors are surfaced via `Kernel.warn`.
12
18
  def run(work)
13
- result = Concurrent::Array.new
19
+ results = Concurrent::Array.new
20
+ errors = Concurrent::Array.new
14
21
 
15
22
  threads = work.map do |w|
16
23
  Thread.new do
17
- result << yield(w)
24
+ results << yield(w)
25
+ rescue StandardError => e
26
+ errors << e
18
27
  end
19
28
  end
20
29
 
21
30
  threads.each(&:join)
22
31
 
23
- result
32
+ if errors.any?
33
+ errors.drop(1).each do |e|
34
+ Kernel.warn("WARNING: parallel task failed: #{e.class}: #{e.message}")
35
+ end
36
+ raise errors.first
37
+ end
38
+
39
+ results
24
40
  end
25
41
  end
26
42
  end
@@ -14,7 +14,8 @@ module Capistrano
14
14
  def set_defaults
15
15
  set_if_empty :asg_rolling_group_name, ENV.fetch('asg_name', nil)
16
16
  set_if_empty :asg_rolling_use_private_ip_address, true
17
- set_if_empty :asg_rolling_verbose, true
17
+ set_if_empty :asg_rolling_log_verbose, true
18
+ set_if_empty :asg_rolling_log_timestamp, false
18
19
  set_if_empty :asg_rolling_update, true
19
20
  end
20
21
 
@@ -28,6 +29,7 @@ module Capistrano
28
29
 
29
30
  after 'rolling:update', 'rolling:cleanup'
30
31
  after 'rolling:create_ami', 'rolling:cleanup'
32
+
31
33
  after 'rolling:update', 'rolling:instance_refresh_status'
32
34
  after 'rolling:trigger_instance_refresh', 'rolling:instance_refresh_status'
33
35
 
@@ -41,7 +43,7 @@ module Capistrano
41
43
  end
42
44
 
43
45
  def logger
44
- @logger ||= Logger.new(verbose: config.verbose?)
46
+ @logger ||= Logger.new(timestamp: config.log_timestamp?, verbose: config.log_verbose?)
45
47
  end
46
48
 
47
49
  def config
@@ -59,7 +61,7 @@ module Capistrano
59
61
 
60
62
  # Make sure any instances that were launched for rolling updates are terminated.
61
63
  def cleanup
62
- instances = config.instances.auto_terminate
64
+ instances = config.instances.auto_terminatable
63
65
  return if instances.empty?
64
66
 
65
67
  logger.info 'Terminating instance(s)...'
@@ -5,7 +5,7 @@ require 'time'
5
5
  module Capistrano
6
6
  module ASG
7
7
  module Rolling
8
- # Test the SSHKit backend for availability.
8
+ # Polls an SSHKit backend until its SSH port is open and accepting connections.
9
9
  module SSH
10
10
  module_function
11
11
 
@@ -3,7 +3,7 @@
3
3
  module Capistrano
4
4
  module ASG
5
5
  module Rolling
6
- VERSION = '0.7.0'
6
+ VERSION = '0.9.0'
7
7
  end
8
8
  end
9
9
  end
@@ -6,7 +6,6 @@ require 'capistrano/asg/rolling/aws'
6
6
  require 'capistrano/asg/rolling/ami'
7
7
  require 'capistrano/asg/rolling/autoscale_group'
8
8
  require 'capistrano/asg/rolling/autoscale_groups'
9
- require 'capistrano/asg/rolling/configuration'
10
9
  require 'capistrano/asg/rolling/dsl'
11
10
  require 'capistrano/asg/rolling/exception'
12
11
  require 'capistrano/asg/rolling/instance'
@@ -15,8 +14,10 @@ require 'capistrano/asg/rolling/launch_template'
15
14
  require 'capistrano/asg/rolling/launch_templates'
16
15
  require 'capistrano/asg/rolling/logger'
17
16
  require 'capistrano/asg/rolling/parallel'
18
- require 'capistrano/asg/rolling/plugin'
19
17
  require 'capistrano/asg/rolling/snapshot'
20
18
  require 'capistrano/asg/rolling/ssh'
21
19
  require 'capistrano/asg/rolling/tags'
22
20
  require 'capistrano/asg/rolling/version'
21
+
22
+ require 'capistrano/asg/rolling/configuration'
23
+ require 'capistrano/asg/rolling/plugin'
@@ -2,35 +2,50 @@
2
2
 
3
3
  namespace :rolling do
4
4
  desc 'Setup servers to be used for (rolling) deployment'
5
- task :setup do
6
- config.autoscale_groups.each do |group|
7
- if group.rolling?
5
+ task setup: %w[rolling:prepare rolling:wait_for_ssh]
6
+
7
+ desc 'Resolve Auto Scaling Groups, launch rolling instances, and register all servers'
8
+ task :prepare do
9
+ rolling_groups = config.autoscale_groups.rolling
10
+ if rolling_groups.any?
11
+ rolling_groups.each do |group|
8
12
  logger.info "Auto Scaling Group: **#{group.name}**, rolling deployment strategy."
13
+ end
9
14
 
10
- # If we've already launched an instance with this image, then skip it.
11
- next unless config.instances.with_image(group.launch_template.image_id).empty?
15
+ launched_instances = Capistrano::ASG::Rolling::Parallel.run(rolling_groups.with_unique_images) do |group|
16
+ Capistrano::ASG::Rolling::Instance.run(
17
+ autoscaling_group: group,
18
+ overrides: config.instance_overrides
19
+ ).tap { |instance| config.instances << instance }
20
+ end
12
21
 
13
- instance = Capistrano::ASG::Rolling::Instance.run(autoscaling_group: group, overrides: config.instance_overrides)
22
+ launched_instances.each do |instance|
14
23
  logger.info "Launched Instance: **#{instance.id}**"
15
- config.instances << instance
16
24
 
25
+ group = instance.autoscale_group
17
26
  add_instance(instance, group.properties)
18
- else
19
- logger.info "Auto Scaling Group: **#{group.name}**, standard deployment strategy."
27
+ end
28
+ end
20
29
 
21
- group.instances.each_with_index do |instance, index|
30
+ standard_groups = config.autoscale_groups.standard
31
+ standard_groups.each do |group|
32
+ logger.info "Auto Scaling Group: **#{group.name}**, standard deployment strategy."
33
+
34
+ group.instances.each_with_index do |instance, index|
35
+ server_properties =
22
36
  if index.zero? && group.properties.key?(:primary_roles)
23
- server_properties = group.properties.dup
24
- server_properties[:roles] = server_properties.delete(:primary_roles)
37
+ group.properties.merge(roles: group.properties[:primary_roles]).except(:primary_roles)
25
38
  else
26
- server_properties = group.properties
39
+ group.properties
27
40
  end
28
41
 
29
- add_instance(instance, server_properties)
30
- end
42
+ add_instance(instance, server_properties)
31
43
  end
32
44
  end
45
+ end
33
46
 
47
+ desc 'Wait for SSH to become available on launched instances'
48
+ task :wait_for_ssh do
34
49
  unless config.instances.empty?
35
50
  logger.info 'Waiting for SSH to be available...'
36
51
 
@@ -54,31 +69,22 @@ namespace :rolling do
54
69
  updated_templates = launch_templates.update(amis: amis, description: revision_log_message)
55
70
 
56
71
  logger.info 'Triggering Instance Refresh on Auto Scaling Group(s)...'
57
- updated_templates.each do |launch_template|
58
- config.autoscale_groups.with_launch_template(launch_template).each do |group|
59
- group.start_instance_refresh(launch_template)
72
+ groups_with_templates = updated_templates.flat_map do |launch_template|
73
+ config.autoscale_groups.with_launch_template(launch_template).map { |group| [group, launch_template] }
74
+ end
60
75
 
61
- logger.verbose "Successfully started Instance Refresh on Auto Scaling Group **#{group.name}**."
62
- rescue Capistrano::ASG::Rolling::StartInstanceRefreshError => e
63
- logger.info "Failed to start Instance Refresh on Auto Scaling Group **#{group.name}**: #{e.message}"
64
- end
76
+ Capistrano::ASG::Rolling::Parallel.run(groups_with_templates) do |group, launch_template|
77
+ group.start_instance_refresh(launch_template)
78
+
79
+ logger.verbose "Successfully started Instance Refresh on Auto Scaling Group **#{group.name}**."
80
+ rescue Capistrano::ASG::Rolling::StartInstanceRefreshError => e
81
+ logger.warning "Failed to start Instance Refresh on Auto Scaling Group **#{group.name}**: #{e.message}"
65
82
  end
66
83
 
67
84
  config.launch_templates.merge(updated_templates)
68
85
  end
69
86
  end
70
87
 
71
- desc 'Trigger instance refresh of deployed autoscaling groups'
72
- task :trigger_instance_refresh do
73
- logger.info 'Triggering Instance Refresh on Auto Scaling Group(s)...'
74
- config.autoscale_groups.each do |group|
75
- group.start_instance_refresh(group.launch_template)
76
- logger.info "Successfully started Instance Refresh on Auto Scaling Group **#{group.name}**."
77
- rescue Capistrano::ASG::Rolling::StartInstanceRefreshError => e
78
- logger.info "Failed to start Instance Refresh on Auto Scaling Group **#{group.name}**: #{e.message}"
79
- end
80
- end
81
-
82
88
  desc 'Clean up old Launch Template versions and AMIs and terminate instances'
83
89
  task :cleanup do
84
90
  unless config.launch_templates.empty?
@@ -114,7 +120,7 @@ namespace :rolling do
114
120
  end
115
121
  end
116
122
 
117
- instances = config.instances.auto_terminate
123
+ instances = config.instances.auto_terminatable
118
124
  if instances.any?
119
125
  logger.info 'Terminating instance(s)...'
120
126
  begin
@@ -176,33 +182,50 @@ namespace :rolling do
176
182
 
177
183
  logger.info 'Updating Launch Template with the new AMI...'
178
184
  launch_template = group.launch_template
179
- launch_template.create_version(image_id: ami.id, description: revision_log_message)
185
+ new_template = launch_template.create_version(image_id: ami.id, description: revision_log_message)
180
186
 
181
- config.launch_templates << launch_template
187
+ config.launch_templates << new_template
182
188
  else
183
189
  logger.error 'Unable to create AMI. No instance with a valid state was found in the Auto Scaling Group.'
184
190
  end
185
191
  end
186
192
  end
187
193
 
194
+ desc 'Trigger instance refresh on all Auto Scaling Groups'
195
+ task :trigger_instance_refresh do
196
+ logger.info 'Triggering Instance Refresh on Auto Scaling Group(s)...'
197
+ Capistrano::ASG::Rolling::Parallel.run(config.autoscale_groups) do |group|
198
+ group.start_instance_refresh(group.launch_template)
199
+
200
+ logger.info "Successfully started Instance Refresh on Auto Scaling Group **#{group.name}**."
201
+ rescue Capistrano::ASG::Rolling::StartInstanceRefreshError => e
202
+ logger.warning "Failed to start Instance Refresh on Auto Scaling Group **#{group.name}**: #{e.message}"
203
+ end
204
+ end
205
+
188
206
  desc 'Get status of instance refresh'
189
207
  task :instance_refresh_status do
190
208
  if config.wait_for_instance_refresh?
191
- groups = config.autoscale_groups.to_h { |group| [group.name, group] }
192
- completed_groups = []
209
+ groups = config.autoscale_groups.select(&:refresh_id).to_h { |group| [group.name, group] }
210
+ completed_refreshes = []
193
211
 
194
212
  while groups.any?
195
213
  groups.each do |name, group|
196
214
  refresh = group.latest_instance_refresh
197
215
  if refresh.nil? || refresh.completed?
198
- logger.info "Auto Scaling Group: **#{name}**, completed with status '#{refresh.status}'." if refresh.completed?
199
- completed_groups.push groups.delete(name)
200
- elsif !refresh.percentage_complete.nil?
216
+ logger.info "Auto Scaling Group: **#{name}**, completed with status '#{refresh.status}'." if refresh&.completed?
217
+
218
+ groups.delete(name)
219
+ completed_refreshes.push(refresh) if refresh
220
+ elsif refresh.percentage_complete
201
221
  logger.info "Auto Scaling Group: **#{name}**, #{refresh.percentage_complete}% completed, status '#{refresh.status}'."
202
222
  else
203
223
  logger.info "Auto Scaling Group: **#{name}**, status '#{refresh.status}'."
204
224
  end
225
+ rescue Capistrano::ASG::Rolling::InstanceRefreshStatusError => e
226
+ logger.warning "Auto Scaling Group: **#{name}**, failed to fetch status: #{e.message} - retrying on next poll."
205
227
  end
228
+
206
229
  next if groups.empty?
207
230
 
208
231
  wait_for = config.instance_refresh_polling_interval
@@ -210,7 +233,7 @@ namespace :rolling do
210
233
  sleep wait_for
211
234
  end
212
235
 
213
- failed = completed_groups.any? { |group| group.latest_instance_refresh.failed? }
236
+ failed = completed_refreshes.any?(&:failed?)
214
237
  raise Capistrano::ASG::Rolling::InstanceRefreshFailed if failed
215
238
  end
216
239
  end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capistrano-asg-rolling
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kentaa
8
+ - iRaiser
8
9
  autorequire:
9
10
  bindir: exe
10
11
  cert_chain: []
11
- date: 2025-12-23 00:00:00.000000000 Z
12
+ date: 2026-08-28 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: bundler
@@ -52,6 +53,20 @@ dependencies:
52
53
  - - "~>"
53
54
  - !ruby/object:Gem::Version
54
55
  version: '3.0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: simplecov
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '1.0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '1.0'
55
70
  - !ruby/object:Gem::Dependency
56
71
  name: webmock
57
72
  requirement: !ruby/object:Gem::Requirement
@@ -130,7 +145,7 @@ dependencies:
130
145
  version: '1'
131
146
  description:
132
147
  email:
133
- - developers@kentaa.nl
148
+ - tech-arnhem@iraiser.eu
134
149
  executables: []
135
150
  extensions: []
136
151
  extra_rdoc_files: []
@@ -181,14 +196,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
181
196
  requirements:
182
197
  - - ">="
183
198
  - !ruby/object:Gem::Version
184
- version: 3.1.0
199
+ version: 3.2.0
185
200
  required_rubygems_version: !ruby/object:Gem::Requirement
186
201
  requirements:
187
202
  - - ">="
188
203
  - !ruby/object:Gem::Version
189
204
  version: '0'
190
205
  requirements: []
191
- rubygems_version: 3.3.27
206
+ rubygems_version: 3.4.19
192
207
  signing_key:
193
208
  specification_version: 4
194
209
  summary: Capistrano plugin for performing rolling updates to AWS Auto Scaling Groups