ecs_deploy 1.0.3 → 1.0.4

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: f4d2cb1f5439702efac5ec26e9cd0c47c6225e0106c05b6f73b7860098098488
4
- data.tar.gz: 29007dc6fdeb7b64b82a0113ac78ad00025ec9693bca1b238ccca3c507065c01
3
+ metadata.gz: a731ec39a6928bb4365fbb4784b91dd92737eb5b5796da19e3be8185d6bc10b8
4
+ data.tar.gz: '04758ae740d9ab1bb9108d5dcae1df72102fd161e0f96a38f1e30b461415bc7f'
5
5
  SHA512:
6
- metadata.gz: 42481f7631cebe62057fbd9e2c0fc6ba61ff0ee198e5d537958934df8a001c76ef5ddb6f8965c2f7db57d0098aac3b79cb43a677cdbaa95609107d29826892b8
7
- data.tar.gz: 28a2d8072e7f94a84301e0f0607fc02c469561d1f39da439cf87d3f7b64c12e301bb77a13bb96e0823870b43af36f5f35ee3e4984567ac57ebe5ae759342d61c
6
+ metadata.gz: 429dc1f441f7a67a973293adf8c48a62671ab3f5710cee596186a7fe5773b772b643b9ef879fb0b2143d7f7100754b6521ec6823094122fec85bfd189ca3c306
7
+ data.tar.gz: 03f568218e5e77e2c9b86560ac5dd4f41e19440f98da1ee102ee09057bc7ec8fdf7caa5366b56d06946d5887a0a60d8bae0c1ce04e80e0a50df7a51e40642da0
@@ -0,0 +1,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ ruby-version: ['2.5', '2.6', '2.7', '3.0', '3.1', '3.2']
14
+
15
+ steps:
16
+ - uses: actions/checkout@v3
17
+ - name: Set up Ruby
18
+ uses: ruby/setup-ruby@v1
19
+ with:
20
+ ruby-version: ${{ matrix.ruby-version }}
21
+ bundler-cache: true
22
+ - name: Run tests
23
+ run: bundle exec rake
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # v1.0
2
2
 
3
+ ## Release v1.0.4 - 2023/02/10
4
+
5
+ ### Bug fixes
6
+
7
+ - Fix Aws::AutoScaling::Errors::ValidationError https://github.com/reproio/ecs_deploy/pull/85
8
+
9
+ - Fix Timeout::Error that occurs in trigger_capacity_update https://github.com/reproio/ecs_deploy/pull/80
10
+
11
+ - use force a new deployment, when switching from launch type to capacity provider strategy on an existing service https://github.com/reproio/ecs_deploy/pull/75
12
+
13
+ ### Enhancement
14
+
15
+ - Run test with Ruby 3.2 https://github.com/reproio/ecs_deploy/pull/83
16
+
17
+ - Merge `propagate_tags` to service_options when updating service https://github.com/reproio/ecs_deploy/pull/82
18
+
19
+ - Show service event logs while waiting for services https://github.com/reproio/ecs_deploy/pull/81
20
+
21
+ - Stop supporting ruby 2.4 https://github.com/reproio/ecs_deploy/pull/79
22
+
23
+ - Display warning that desired count has reached max value https://github.com/reproio/ecs_deploy/pull/78
24
+
25
+ - Make draining feature opt-outable https://github.com/reproio/ecs_deploy/pull/77
26
+
27
+ - Add capacity_provider_strategy options to Service https://github.com/reproio/ecs_deploy/pull/74
28
+
3
29
  ## Release v1.0.3 - 2021/11/17
4
30
 
5
31
  ### Bug fixes
data/README.md CHANGED
@@ -215,6 +215,7 @@ auto_scaling_groups:
215
215
  # autoscaler will set the capacity to (buffer + desired_tasks * required_capacity).
216
216
  # Adjust this value if it takes much time to prepare ECS instances and launch new tasks.
217
217
  buffer: 1
218
+ disable_draining: false # cf. spot_instance_intrp_warns_queue_urls
218
219
  services:
219
220
  - name: repro-api-production
220
221
  step: 1
@@ -242,6 +243,7 @@ spot_fleet_requests:
242
243
  region: ap-northeast-1
243
244
  cluster: ecs-cluster-for-worker
244
245
  buffer: 1
246
+ disable_draining: false # cf. spot_instance_intrp_warns_queue_urls
245
247
  services:
246
248
  - name: repro-worker-production
247
249
  step: 1
@@ -261,11 +263,15 @@ spot_fleet_requests:
261
263
  state: ALARM
262
264
  prioritized_over_upscale_triggers: true
263
265
 
264
- # If you specify `spot_instance_intrp_warns_queue_urls` as SQS queue for spot instance interruption warnings,
265
- # autoscaler will polls them and set the state of instances to be intrrupted to "DRAINING".
266
- # autoscaler will also waits for the capacity of active instances in the cluster being decreased
267
- # when the capacity of spot fleet request is decreased,
268
- # so you should specify URLs or set the state of the instances to "DRAINING" manually.
266
+ # When you use spot instances, instances that receive interruption warnings should be drained.
267
+ # If you set URLs of SQS queues for spot instance interruption warnings to `spot_instance_intrp_warns_queue_urls`,
268
+ # autoscaler drains instances to interrupt and detaches the instances from the auto scaling groups with
269
+ # should_decrement_desired_capacity false.
270
+ # If you set ECS_ENABLE_SPOT_INSTANCE_DRAINING to true, we recommend that you opt out of the draining feature
271
+ # by setting disable_draining to true in the configurations of auto scaling groups and spot fleet requests.
272
+ # Otherwise, instances don't seem to be drained on rare occasions.
273
+ # Even if you opt out of the feature, you still have the advantage of setting `spot_instance_intrp_warns_queue_urls`
274
+ # because instances to interrupt are replaced with new instances as soon as possible.
269
275
  spot_instance_intrp_warns_queue_urls:
270
276
  - https://sqs.ap-northeast-1.amazonaws.com/<account-id>/spot-instance-intrp-warns
271
277
  ```
data/ecs_deploy.gemspec CHANGED
@@ -30,4 +30,5 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency "bundler", ">= 1.11", "< 3"
31
31
  spec.add_development_dependency "rake", ">= 10.0"
32
32
  spec.add_development_dependency "rspec", "~> 3.0"
33
+ spec.add_development_dependency "rexml" # For aws-sdk-*
33
34
  end
@@ -7,7 +7,7 @@ require "ecs_deploy/auto_scaler/cluster_resource_manager"
7
7
 
8
8
  module EcsDeploy
9
9
  module AutoScaler
10
- AutoScalingGroupConfig = Struct.new(:name, :region, :cluster, :buffer, :service_configs) do
10
+ AutoScalingGroupConfig = Struct.new(:name, :region, :cluster, :buffer, :service_configs, :disable_draining) do
11
11
  include ConfigBase
12
12
 
13
13
  MAX_DETACHABLE_INSTANCE_COUNT = 20
@@ -82,7 +82,7 @@ module EcsDeploy
82
82
  def decrease_desired_capacity(count)
83
83
  container_instance_arns_in_service = cluster_resource_manager.fetch_container_instance_arns_in_service
84
84
  container_instances_in_cluster = cluster_resource_manager.fetch_container_instances_in_cluster
85
- auto_scaling_group_instances = instances(reload: true)
85
+ auto_scaling_group_instances = describe_detachable_instances
86
86
  deregisterable_instances = container_instances_in_cluster.select do |i|
87
87
  i.pending_tasks_count == 0 &&
88
88
  !running_essential_task?(i, container_instance_arns_in_service) &&
@@ -144,15 +144,8 @@ module EcsDeploy
144
144
 
145
145
  def detach_and_terminate_orphan_instances
146
146
  container_instance_ids = cluster_resource_manager.fetch_container_instances_in_cluster.map(&:ec2_instance_id)
147
- orphans = instances(reload: true).reject do |i|
147
+ orphans = describe_detachable_instances.reject do |i|
148
148
  next true if container_instance_ids.include?(i.instance_id)
149
-
150
- # The lifecycle state of terminated instances becomes "Terminating", "Terminating:Wait", or "Terminating:Proceed",
151
- # and we can't detach instances in such a state.
152
- if i.lifecycle_state.start_with?("Terminating")
153
- AutoScaler.error_logger.warn("#{log_prefix} The lifesycle state of #{i.instance_id} is \"#{i.lifecycle_state}\", so ignore it")
154
- next true
155
- end
156
149
  end.map(&:instance_id)
157
150
 
158
151
  return if orphans.empty?
@@ -184,14 +177,11 @@ module EcsDeploy
184
177
  )
185
178
  end
186
179
 
187
- def instances(reload: false)
188
- if reload || @instances.nil?
189
- resp = client.describe_auto_scaling_groups({
190
- auto_scaling_group_names: [name],
191
- })
192
- @instances = resp.auto_scaling_groups[0].instances
193
- else
194
- @instances
180
+ def describe_detachable_instances
181
+ client.describe_auto_scaling_groups({ auto_scaling_group_names: [name] }).auto_scaling_groups[0].instances.reject do |i|
182
+ # The lifecycle state of terminated instances becomes "Detaching", "Terminating", "Terminating:Wait", or "Terminating:Proceed",
183
+ # and we can't detach instances in such a state.
184
+ i.lifecycle_state.start_with?("Terminating") || i.lifecycle_state == "Detaching"
195
185
  end
196
186
  end
197
187
 
@@ -74,39 +74,42 @@ module EcsDeploy
74
74
  end
75
75
 
76
76
  def trigger_capacity_update(old_desired_capacity, new_desired_capacity, interval: 5, wait_until_capacity_updated: false)
77
+ return if new_desired_capacity == old_desired_capacity
78
+
77
79
  th = Thread.new do
78
80
  @logger&.info "#{log_prefix} Start updating capacity: #{old_desired_capacity} -> #{new_desired_capacity}"
79
81
  Timeout.timeout(180) do
80
- until @capacity == new_desired_capacity || (new_desired_capacity >= old_desired_capacity && @capacity > new_desired_capacity)
82
+ until @capacity == new_desired_capacity ||
83
+ (new_desired_capacity > old_desired_capacity && @capacity > new_desired_capacity) ||
84
+ (new_desired_capacity < old_desired_capacity && @capacity < new_desired_capacity)
81
85
  @mutex.synchronize do
82
- begin
83
- @capacity = calculate_active_instance_capacity
84
- @resource.broadcast
85
- rescue => e
86
- AutoScaler.error_logger.warn("#{log_prefix} `#{__method__}': #{e} (#{e.class})")
87
- end
86
+ @capacity = calculate_active_instance_capacity
87
+ @resource.broadcast
88
+ rescue => e
89
+ AutoScaler.error_logger.warn("#{log_prefix} `#{__method__}': #{e} (#{e.class})")
88
90
  end
89
91
 
90
92
  sleep interval
91
93
  end
92
94
  @logger&.info "#{log_prefix} capacity is updated to #{@capacity}"
93
95
  end
96
+ rescue Timeout::Error => e
97
+ msg = "#{log_prefix} `#{__method__}': #{e} (#{e.class})"
98
+ if @capacity_based_on == "vCPUs"
99
+ # Timeout::Error sometimes occur.
100
+ # For example, the following case never meats the condition of until
101
+ # * old_desired_capaacity is 102
102
+ # * new_desired_capaacity is 101
103
+ # * all instances have 2 vCPUs
104
+ AutoScaler.error_logger.warn(msg)
105
+ else
106
+ AutoScaler.error_logger.error(msg)
107
+ end
94
108
  end
95
109
 
96
110
  if wait_until_capacity_updated
97
111
  @logger&.info "#{log_prefix} Wait for the capacity of active instances to become #{new_desired_capacity} from #{old_desired_capacity}"
98
- begin
99
- th.join
100
- rescue Timeout::Error => e
101
- msg = "#{log_prefix} `#{__method__}': #{e} (#{e.class})"
102
- if @capacity_based_on == "vCPUs"
103
- # Timeout::Error sometimes occur.
104
- # For example, @capacity won't be new_desired_capacity if new_desired_capacity is odd and all instances have 2 vCPUs
105
- AutoScaler.error_logger.warn(msg)
106
- else
107
- AutoScaler.error_logger.error(msg)
108
- end
109
- end
112
+ th.join
110
113
  end
111
114
  end
112
115
 
@@ -78,6 +78,11 @@ module EcsDeploy
78
78
  def set_instance_state_to_draining(config_to_instance_ids, region)
79
79
  cl = ecs_client(region)
80
80
  config_to_instance_ids.each do |config, instance_ids|
81
+ if config.disable_draining == true || config.disable_draining == "true"
82
+ @logger.info "Skip draining instances: region: #{region}, cluster: #{config.cluster}, instance_ids: #{instance_ids.inspect}"
83
+ next
84
+ end
85
+
81
86
  arns = cl.list_container_instances(
82
87
  cluster: config.cluster,
83
88
  filter: "ec2InstanceId in [#{instance_ids.join(",")}]",
@@ -135,6 +135,9 @@ module EcsDeploy
135
135
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
136
136
  @reach_max_at ||= now
137
137
  @logger.info "#{log_prefix} Service waits cooldown elapsed #{(now - @reach_max_at).to_i}sec"
138
+ if next_desired_count > max_task_count[current_level] && current_level == max_task_count.size - 1
139
+ @logger.warn "#{log_prefix} Desired count has reached the maximum value and couldn't be increased"
140
+ end
138
141
  elsif current_level == next_level && next_desired_count < max_task_count[current_level]
139
142
  level = current_level
140
143
  @reach_max_at = nil
@@ -8,7 +8,7 @@ require "ecs_deploy/auto_scaler/cluster_resource_manager"
8
8
 
9
9
  module EcsDeploy
10
10
  module AutoScaler
11
- SpotFleetRequestConfig = Struct.new(:id, :region, :cluster, :buffer, :service_configs) do
11
+ SpotFleetRequestConfig = Struct.new(:id, :region, :cluster, :buffer, :service_configs, :disable_draining) do
12
12
  include ConfigBase
13
13
 
14
14
  def initialize(attributes = {}, logger)
@@ -113,6 +113,7 @@ namespace :ecs do
113
113
  service_options[:deployment_configuration] = service[:deployment_configuration] if service[:deployment_configuration]
114
114
  service_options[:placement_constraints] = service[:placement_constraints] if service[:placement_constraints]
115
115
  service_options[:placement_strategy] = service[:placement_strategy] if service[:placement_strategy]
116
+ service_options[:capacity_provider_strategy] = service[:capacity_provider_strategy] if service[:capacity_provider_strategy]
116
117
  service_options[:scheduling_strategy] = service[:scheduling_strategy] if service[:scheduling_strategy]
117
118
  s = EcsDeploy::Service.new(**service_options)
118
119
  s.deploy
@@ -179,6 +180,7 @@ namespace :ecs do
179
180
  service_options[:deployment_configuration] = service[:deployment_configuration] if service[:deployment_configuration]
180
181
  service_options[:placement_constraints] = service[:placement_constraints] if service[:placement_constraints]
181
182
  service_options[:placement_strategy] = service[:placement_strategy] if service[:placement_strategy]
183
+ service_options[:capacity_provider_strategy] = service[:capacity_provider_strategy] if service[:capacity_provider_strategy]
182
184
  s = EcsDeploy::Service.new(**service_options)
183
185
  s.deploy
184
186
  EcsDeploy::TaskDefinition.deregister(current_task_definition_arn, region: r)
@@ -7,7 +7,7 @@ module EcsDeploy
7
7
 
8
8
  class TooManyAttemptsError < StandardError; end
9
9
 
10
- attr_reader :cluster, :region, :service_name, :delete
10
+ attr_reader :cluster, :region, :service_name, :delete, :deploy_started_at
11
11
 
12
12
  def initialize(
13
13
  cluster:, service_name:, task_definition_name: nil, revision: nil,
@@ -16,6 +16,7 @@ module EcsDeploy
16
16
  launch_type: nil,
17
17
  placement_constraints: [],
18
18
  placement_strategy: [],
19
+ capacity_provider_strategy: nil,
19
20
  network_configuration: nil,
20
21
  health_check_grace_period_seconds: nil,
21
22
  scheduling_strategy: 'REPLICA',
@@ -35,6 +36,7 @@ module EcsDeploy
35
36
  @launch_type = launch_type
36
37
  @placement_constraints = placement_constraints
37
38
  @placement_strategy = placement_strategy
39
+ @capacity_provider_strategy = capacity_provider_strategy
38
40
  @network_configuration = network_configuration
39
41
  @health_check_grace_period_seconds = health_check_grace_period_seconds
40
42
  @scheduling_strategy = scheduling_strategy
@@ -59,6 +61,7 @@ module EcsDeploy
59
61
  end
60
62
 
61
63
  def deploy
64
+ @deploy_started_at = Time.now
62
65
  res = @client.describe_services(cluster: @cluster, services: [@service_name])
63
66
  service_options = {
64
67
  cluster: @cluster,
@@ -66,8 +69,25 @@ module EcsDeploy
66
69
  deployment_configuration: @deployment_configuration,
67
70
  network_configuration: @network_configuration,
68
71
  health_check_grace_period_seconds: @health_check_grace_period_seconds,
72
+ capacity_provider_strategy: @capacity_provider_strategy,
69
73
  enable_execute_command: @enable_execute_command,
74
+ enable_ecs_managed_tags: @enable_ecs_managed_tags,
75
+ placement_constraints: @placement_constraints,
76
+ placement_strategy: @placement_strategy,
70
77
  }
78
+
79
+ if @load_balancers && EcsDeploy.config.ecs_service_role
80
+ service_options.merge!({
81
+ role: EcsDeploy.config.ecs_service_role,
82
+ })
83
+ end
84
+
85
+ if @load_balancers
86
+ service_options.merge!({
87
+ load_balancers: @load_balancers,
88
+ })
89
+ end
90
+
71
91
  if res.services.select{ |s| s.status == 'ACTIVE' }.empty?
72
92
  return if @delete
73
93
 
@@ -75,25 +95,10 @@ module EcsDeploy
75
95
  service_name: @service_name,
76
96
  desired_count: @desired_count.to_i,
77
97
  launch_type: @launch_type,
78
- placement_constraints: @placement_constraints,
79
- placement_strategy: @placement_strategy,
80
- enable_ecs_managed_tags: @enable_ecs_managed_tags,
81
98
  tags: @tags,
82
99
  propagate_tags: @propagate_tags,
83
100
  })
84
101
 
85
- if @load_balancers && EcsDeploy.config.ecs_service_role
86
- service_options.merge!({
87
- role: EcsDeploy.config.ecs_service_role,
88
- })
89
- end
90
-
91
- if @load_balancers
92
- service_options.merge!({
93
- load_balancers: @load_balancers,
94
- })
95
- end
96
-
97
102
  if @scheduling_strategy == 'DAEMON'
98
103
  service_options[:scheduling_strategy] = @scheduling_strategy
99
104
  service_options.delete(:desired_count)
@@ -105,12 +110,34 @@ module EcsDeploy
105
110
 
106
111
  service_options.merge!({service: @service_name})
107
112
  service_options.merge!({desired_count: @desired_count}) if @desired_count
113
+ service_options.merge!({propagate_tags: @propagate_tags}) if @propagate_tags
114
+
115
+ current_service = res.services[0]
116
+ service_options.merge!({force_new_deployment: true}) if need_force_new_deployment?(current_service)
117
+
108
118
  update_tags(@service_name, @tags)
109
119
  @response = @client.update_service(service_options)
110
120
  EcsDeploy.logger.info "update service [#{@service_name}] [#{@cluster}] [#{@region}] [#{Paint['OK', :green]}]"
111
121
  end
112
122
  end
113
123
 
124
+ private def need_force_new_deployment?(service)
125
+ return false unless @capacity_provider_strategy
126
+ return true unless service.capacity_provider_strategy
127
+
128
+ return true if @capacity_provider_strategy.size != service.capacity_provider_strategy.size
129
+
130
+ match_array = @capacity_provider_strategy.all? do |strategy|
131
+ service.capacity_provider_strategy.find do |current_strategy|
132
+ strategy[:capacity_provider] == current_strategy.capacity_provider &&
133
+ strategy[:weight] == current_strategy.weight &&
134
+ strategy[:base] == current_strategy.base
135
+ end
136
+ end
137
+
138
+ !match_array
139
+ end
140
+
114
141
  def delete_service
115
142
  if @scheduling_strategy != 'DAEMON'
116
143
  @client.update_service(cluster: @cluster, service: @service_name, desired_count: 0)
@@ -142,6 +169,16 @@ module EcsDeploy
142
169
  end
143
170
  end
144
171
 
172
+ def log_events(ecs_service)
173
+ ecs_service.events.sort_by(&:created_at).each do |e|
174
+ next if e.created_at <= deploy_started_at
175
+ next if @last_event && e.created_at <= @last_event.created_at
176
+
177
+ EcsDeploy.logger.info e.message
178
+ @last_event = e
179
+ end
180
+ end
181
+
145
182
  def self.wait_all_running(services)
146
183
  services.group_by { |s| [s.cluster, s.region] }.flat_map do |(cl, region), ss|
147
184
  client = Aws::ECS::Client.new(region: region)
@@ -155,6 +192,8 @@ module EcsDeploy
155
192
  if s.deployments.size == 1 && s.running_count == s.desired_count
156
193
  chunked_service_names.delete(s.service_name)
157
194
  end
195
+ service = ss.detect {|sc| sc.service_name == s.service_name }
196
+ service.log_events(s)
158
197
  end
159
198
  break if chunked_service_names.empty?
160
199
  sleep EcsDeploy.config.ecs_wait_until_services_stable_delay
@@ -1,3 +1,3 @@
1
1
  module EcsDeploy
2
- VERSION = "1.0.3"
2
+ VERSION = "1.0.4"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ecs_deploy
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - joker1007
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-11-17 00:00:00.000000000 Z
11
+ date: 2023-02-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-autoscaling
@@ -170,6 +170,20 @@ dependencies:
170
170
  - - "~>"
171
171
  - !ruby/object:Gem::Version
172
172
  version: '3.0'
173
+ - !ruby/object:Gem::Dependency
174
+ name: rexml
175
+ requirement: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ type: :development
181
+ prerelease: false
182
+ version_requirements: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
173
187
  description: AWS ECS deploy helper
174
188
  email:
175
189
  - kakyoin.hierophant@gmail.com
@@ -178,8 +192,8 @@ executables:
178
192
  extensions: []
179
193
  extra_rdoc_files: []
180
194
  files:
195
+ - ".github/workflows/test.yml"
181
196
  - ".gitignore"
182
- - ".travis.yml"
183
197
  - CHANGELOG.md
184
198
  - Gemfile
185
199
  - README.md
@@ -222,7 +236,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
222
236
  - !ruby/object:Gem::Version
223
237
  version: '0'
224
238
  requirements: []
225
- rubygems_version: 3.2.15
239
+ rubygems_version: 3.4.2
226
240
  signing_key:
227
241
  specification_version: 4
228
242
  summary: AWS ECS deploy helper
data/.travis.yml DELETED
@@ -1,5 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - ruby-2.4.9
4
- - ruby-2.5.7
5
- - ruby-2.6.5