Class: EcsDeploy::Service

Inherits:
Object
  • Object
show all
Defined in:
lib/ecs_deploy/service.rb

Defined Under Namespace

Classes: TooManyAttemptsError

Constant Summary collapse

CHECK_INTERVAL =
5
MAX_DESCRIBE_SERVICES =
10
CREATE_ONLY_KEYS =

Options that Aws::ECS::Client#update_service will not honor on an existing service:

  • launch_type / scheduling_strategy: not in update_service's parameter list
  • role / client_token: create-only, no update-side equivalent
  • deployment_controller: accepted by update_service in aws-sdk-ecs 1.238+ but AWS rejects any change to the controller type at runtime
%i[launch_type scheduling_strategy role client_token deployment_controller].freeze
VALID_WAIT_STRATEGIES =
%i[legacy none service_deployment].freeze
ECS_NATIVE_BLUE_GREEN_STRATEGIES =
%w[BLUE_GREEN LINEAR CANARY].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(cluster:, service_name:, region: nil, **options) ⇒ Service

Returns a new instance of Service.



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/ecs_deploy/service.rb', line 23

def initialize(cluster:, service_name:, region: nil, **options)
  @cluster = cluster
  @service_name = service_name
  @options = options.dup
  @task_definition_name = @options.delete(:task_definition_name) || service_name
  @revision = @options.delete(:revision)
  @delete = @options.delete(:delete) || false
  @wait_strategy = @options.delete(:wait_strategy)
  # Snapshot the keys the user actually passed in, so warnings only fire on
  # options the caller explicitly set (not on defaults injected below).
  @user_provided_keys = (options.keys - %i[task_definition_name revision delete wait_strategy]).freeze
  if @wait_strategy && !VALID_WAIT_STRATEGIES.include?(@wait_strategy)
    raise ArgumentError, "Invalid wait_strategy #{@wait_strategy.inspect}, expected nil or one of #{VALID_WAIT_STRATEGIES.inspect}"
  end
  @options[:deployment_configuration] ||= {maximum_percent: 200, minimum_healthy_percent: 100}
  @options[:placement_constraints] ||= []
  @options[:placement_strategy] ||= []
  @options[:scheduling_strategy] ||= 'REPLICA'
  @options[:enable_execute_command] ||= false

  @response = nil

  region ||= EcsDeploy.config.default_region
  params ||= EcsDeploy.config.ecs_client_params
  @client = region ? Aws::ECS::Client.new(params.merge(region: region)) : Aws::ECS::Client.new(params)
  @region = @client.config.region
end

Instance Attribute Details

#clusterObject (readonly)

Returns the value of attribute cluster.



10
11
12
# File 'lib/ecs_deploy/service.rb', line 10

def cluster
  @cluster
end

#deleteObject (readonly)

Returns the value of attribute delete.



10
11
12
# File 'lib/ecs_deploy/service.rb', line 10

def delete
  @delete
end

#deploy_started_atObject (readonly)

Returns the value of attribute deploy_started_at.



10
11
12
# File 'lib/ecs_deploy/service.rb', line 10

def deploy_started_at
  @deploy_started_at
end

#regionObject (readonly)

Returns the value of attribute region.



10
11
12
# File 'lib/ecs_deploy/service.rb', line 10

def region
  @region
end

#service_nameObject (readonly)

Returns the value of attribute service_name.



10
11
12
# File 'lib/ecs_deploy/service.rb', line 10

def service_name
  @service_name
end

#wait_strategyObject (readonly)

Returns the value of attribute wait_strategy.



10
11
12
# File 'lib/ecs_deploy/service.rb', line 10

def wait_strategy
  @wait_strategy
end

Class Method Details

.legacy_threads(client, cluster, all_services, targets) ⇒ Object



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/ecs_deploy/service.rb', line 244

def self.legacy_threads(client, cluster, all_services, targets)
  targets.map(&:service_name).each_slice(MAX_DESCRIBE_SERVICES).map do |chunked_service_names|
    Thread.new do
      EcsDeploy.config.ecs_wait_until_services_stable_max_attempts.times do
        EcsDeploy.logger.info "waiting for services to stabilize [#{chunked_service_names.join(", ")}] [#{cluster}]"
        resp = client.describe_services(cluster: cluster, services: chunked_service_names)
        resp.services.each do |s|
          # cf. https://github.com/aws/aws-sdk-ruby/blob/master/gems/aws-sdk-ecs/lib/aws-sdk-ecs/waiters.rb#L91-L96
          if s.deployments.size == 1 && s.running_count == s.desired_count
            chunked_service_names.delete(s.service_name)
          end
          service = all_services.detect { |sc| sc.service_name == s.service_name }
          service&.log_events(s)
        end
        break if chunked_service_names.empty?
        sleep EcsDeploy.config.ecs_wait_until_services_stable_delay
      end
      raise TooManyAttemptsError unless chunked_service_names.empty?
    end
  end
end

.service_deployment_threads(client, cluster, targets) ⇒ Object



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/ecs_deploy/service.rb', line 266

def self.service_deployment_threads(client, cluster, targets)
  targets.map do |service|
    Thread.new do
      pending = true
      EcsDeploy.config.ecs_wait_until_services_stable_max_attempts.times do
        EcsDeploy.logger.info "waiting for service deployment to settle [#{service.service_name}] [#{cluster}]"
        arns = client.list_service_deployments(
          cluster: cluster,
          service: service.service_name,
          status: %w[IN_PROGRESS PENDING],
        ).service_deployments.map(&:service_deployment_arn)
        if arns.empty?
          pending = false
          break
        end
        sleep EcsDeploy.config.ecs_wait_until_services_stable_delay
      end
      raise TooManyAttemptsError if pending
    end
  end
end

.wait_all_running(services) ⇒ Object



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/ecs_deploy/service.rb', line 222

def self.wait_all_running(services)
  threads = services.group_by { |s| [s.cluster, s.region] }.flat_map do |(cl, region), ss|
    params = EcsDeploy.config.ecs_client_params
    client = Aws::ECS::Client.new(params.merge(region: region))

    targets = ss.reject(&:delete).reject do |s|
      if s.skip_wait?
        EcsDeploy.logger.info "skip waiting for service [#{s.service_name}] [#{cl}]: ECS-managed deployment, monitor via ecs:describe_deployment"
        true
      else
        false
      end
    end

    legacy_targets = targets.reject { |s| s.wait_strategy == :service_deployment }
    sd_targets = targets.select { |s| s.wait_strategy == :service_deployment }

    legacy_threads(client, cl, ss, legacy_targets) + service_deployment_threads(client, cl, sd_targets)
  end
  threads.each(&:join)
end

Instance Method Details

#current_task_definition_arnObject



51
52
53
54
# File 'lib/ecs_deploy/service.rb', line 51

def current_task_definition_arn
  res = @client.describe_services(cluster: @cluster, services: [@service_name])
  res.services[0].task_definition
end

#delete_serviceObject



163
164
165
166
167
168
169
170
# File 'lib/ecs_deploy/service.rb', line 163

def delete_service
  if @options[:scheduling_strategy] != 'DAEMON'
    @client.update_service(cluster: @cluster, service: @service_name, desired_count: 0)
    sleep 1
  end
  @client.delete_service(cluster: @cluster, service: @service_name)
  EcsDeploy.logger.info "deleted service [#{@service_name}] [#{@cluster}] [#{@region}] [#{Paint['OK', :green]}]"
end

#deployObject



56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/ecs_deploy/service.rb', line 56

def deploy
  @deploy_started_at = Time.now
  res = @client.describe_services(cluster: @cluster, services: [@service_name])

  if res.services.select{ |s| s.status == 'ACTIVE' }.empty?
    return if @delete
    create_service
  else
    return delete_service if @delete
    update_service(res.services[0])
  end
end

#log_events(ecs_service) ⇒ Object



194
195
196
197
198
199
200
201
202
# File 'lib/ecs_deploy/service.rb', line 194

def log_events(ecs_service)
  ecs_service.events.sort_by(&:created_at).each do |e|
    next if e.created_at <= deploy_started_at
    next if @last_event && e.created_at <= @last_event.created_at

    EcsDeploy.logger.info e.message
    @last_event = e
  end
end

#skip_wait?Boolean

Returns:

  • (Boolean)


204
205
206
207
208
209
210
211
212
213
# File 'lib/ecs_deploy/service.rb', line 204

def skip_wait?
  case @wait_strategy
  when :none
    true
  when :legacy, :service_deployment
    false
  when nil
    ecs_native_blue_green?
  end
end

#update_tags(service_name, tags) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/ecs_deploy/service.rb', line 172

def update_tags(service_name, tags)
  service_arn = @client.describe_services(cluster: @cluster, services: [service_name]).services.first.service_arn
  if service_arn.split('/').size == 2
    if tags
      EcsDeploy.logger.warn "#{service_name} doesn't support tagging operations, so tags are ignored. Long arn format must be used for tagging operations."
    end
    return
  end

  tags ||= []
  current_tag_keys = @client.list_tags_for_resource(resource_arn: service_arn).tags.map(&:key)
  deleted_tag_keys = current_tag_keys - tags.map { |t| t[:key] }

  unless deleted_tag_keys.empty?
    @client.untag_resource(resource_arn: service_arn, tag_keys: deleted_tag_keys)
  end

  unless tags.empty?
    @client.tag_resource(resource_arn: service_arn, tags: tags)
  end
end