ecs_deploy 0.3.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,12 +6,13 @@ module EcsDeploy
6
6
  :secret_access_key,
7
7
  :default_region,
8
8
  :deploy_wait_timeout,
9
- :ecs_service_role
9
+ :ecs_service_role,
10
+ :ecs_wait_until_services_stable_max_attempts,
11
+ :ecs_wait_until_services_stable_delay
10
12
 
11
13
  def initialize
12
14
  @log_level = :info
13
15
  @deploy_wait_timeout = 300
14
- @ecs_service_role = "ecsServiceRole"
15
16
  end
16
17
  end
17
18
  end
@@ -0,0 +1,173 @@
1
+ require "aws-sdk-autoscaling"
2
+ require "aws-sdk-ec2"
3
+ require "aws-sdk-ecs"
4
+
5
+ module EcsDeploy
6
+ class InstanceFluctuationManager
7
+ attr_reader :logger
8
+
9
+ MAX_UPDATABLE_ECS_CONTAINER_COUNT = 10
10
+ MAX_DETACHEABLE_EC2_INSTACE_COUNT = 20
11
+
12
+ def initialize(region:, cluster:, auto_scaling_group_name:, desired_capacity:, logger:)
13
+ @region = region
14
+ @cluster = cluster
15
+ @auto_scaling_group_name = auto_scaling_group_name
16
+ @desired_capacity = desired_capacity
17
+ @logger = logger
18
+ end
19
+
20
+ def increase
21
+ asg = as_client.describe_auto_scaling_groups(auto_scaling_group_names: [@auto_scaling_group_name]).auto_scaling_groups.first
22
+
23
+ @logger.info("Increase desired capacity of #{@auto_scaling_group_name}: #{asg.desired_capacity} => #{asg.max_size}")
24
+ as_client.update_auto_scaling_group(auto_scaling_group_name: @auto_scaling_group_name, desired_capacity: asg.max_size)
25
+
26
+ # Run in background because increasing instances may take time
27
+ Thread.new do
28
+ loop do
29
+ cluster = ecs_client.describe_clusters(clusters: [@cluster]).clusters.first
30
+ instance_count = cluster.registered_container_instances_count
31
+ if instance_count == asg.max_size
32
+ @logger.info("Succeeded in increasing instances!")
33
+ break
34
+ end
35
+ @logger.info("Current registered instance count: #{instance_count}")
36
+ sleep 5
37
+ end
38
+ end
39
+ end
40
+
41
+ def decrease
42
+ asg = as_client.describe_auto_scaling_groups(auto_scaling_group_names: [@auto_scaling_group_name]).auto_scaling_groups.first
43
+
44
+ decrease_count = asg.desired_capacity - @desired_capacity
45
+ if decrease_count <= 0
46
+ @logger.info("The capacity is already #{asg.desired_capacity}")
47
+ return
48
+ end
49
+ @logger.info("Decrease desired capacity of #{@auto_scaling_group_name}: #{asg.desired_capacity} => #{@desired_capacity}")
50
+
51
+ container_instance_arns = ecs_client.list_container_instances(
52
+ cluster: @cluster
53
+ ).container_instance_arns
54
+ container_instances = ecs_client.describe_container_instances(
55
+ cluster: @cluster,
56
+ container_instances: container_instance_arns
57
+ ).container_instances
58
+
59
+ az_to_container_instances = container_instances.sort_by {|ci| - ci.running_tasks_count }.group_by do |ci|
60
+ ci.attributes.find {|attribute| attribute.name == "ecs.availability-zone" }.value
61
+ end
62
+ if az_to_container_instances.empty?
63
+ @logger.info("There are no instances to terminate.")
64
+ return
65
+ end
66
+
67
+ target_container_instances = extract_target_container_instances(decrease_count, az_to_container_instances)
68
+
69
+ threads = []
70
+ all_running_task_arns = []
71
+ target_container_instances.map(&:container_instance_arn).each_slice(MAX_UPDATABLE_ECS_CONTAINER_COUNT) do |arns|
72
+ ecs_client.update_container_instances_state(
73
+ cluster: @cluster,
74
+ container_instances: arns,
75
+ status: "DRAINING"
76
+ )
77
+ arns.each do |arn|
78
+ threads << Thread.new(arn) do |a|
79
+ all_running_task_arns.concat(stop_tasks(a))
80
+ end
81
+ end
82
+ end
83
+
84
+ threads.each(&:join)
85
+ ecs_client.wait_until(:tasks_stopped, cluster: @cluster, tasks: all_running_task_arns) unless all_running_task_arns.empty?
86
+ @logger.info("All running tasks are stopped")
87
+
88
+ instance_ids = target_container_instances.map(&:ec2_instance_id)
89
+ terminate_instances(instance_ids)
90
+ @logger.info("Succeeded in decreasing instances!")
91
+ end
92
+
93
+ private
94
+
95
+ def aws_params
96
+ {
97
+ access_key_id: EcsDeploy.config.access_key_id,
98
+ secret_access_key: EcsDeploy.config.secret_access_key,
99
+ region: @region,
100
+ logger: @logger
101
+ }.reject do |_key, value|
102
+ value.nil?
103
+ end
104
+ end
105
+
106
+ def as_client
107
+ @as_client ||= Aws::AutoScaling::Client.new(aws_params)
108
+ end
109
+
110
+ def ec2_client
111
+ @ec2_client ||= Aws::EC2::Client.new(aws_params)
112
+ end
113
+
114
+ def ecs_client
115
+ @ecs_client ||= Aws::ECS::Client.new(aws_params)
116
+ end
117
+
118
+ # Extract container instances to terminate considering AZ balance
119
+ def extract_target_container_instances(decrease_count, az_to_container_instances)
120
+ target_container_instances = []
121
+ decrease_count.times do
122
+ @logger.debug do
123
+ "AZ balance: #{az_to_container_instances.sort_by {|az, _| az }.map {|az, instances| [az, instances.size] }.to_h}"
124
+ end
125
+ az = az_to_container_instances.max_by {|_az, instances| instances.size }.first
126
+ target_container_instances << az_to_container_instances[az].pop
127
+ end
128
+ @logger.info do
129
+ "AZ balance: #{az_to_container_instances.sort_by {|az, _| az }.map {|az, instances| [az, instances.size] }.to_h}"
130
+ end
131
+
132
+ target_container_instances
133
+ end
134
+
135
+ def stop_tasks(arn)
136
+ running_task_arns = ecs_client.list_tasks(cluster: @cluster, container_instance: arn).task_arns
137
+ unless running_task_arns.empty?
138
+ running_tasks = ecs_client.describe_tasks(cluster: @cluster, tasks: running_task_arns).tasks
139
+ running_tasks.each do |task|
140
+ ecs_client.stop_task(cluster: @cluster, task: task.task_arn) if task.group.start_with?("family:")
141
+ end
142
+ end
143
+ @logger.info("Tasks running on #{arn.split('/').last} will be stopped")
144
+ running_task_arns
145
+ end
146
+
147
+ def terminate_instances(instance_ids)
148
+ if instance_ids.empty?
149
+ @logger.info("There are no instances to terminate.")
150
+ return
151
+ end
152
+ instance_ids.each_slice(MAX_DETACHEABLE_EC2_INSTACE_COUNT) do |ids|
153
+ as_client.detach_instances(
154
+ auto_scaling_group_name: @auto_scaling_group_name,
155
+ instance_ids: ids,
156
+ should_decrement_desired_capacity: true
157
+ )
158
+ end
159
+
160
+ ec2_client.terminate_instances(instance_ids: instance_ids)
161
+
162
+ ec2_client.wait_until(:instance_terminated, instance_ids: instance_ids) do |w|
163
+ w.before_wait do |attempts, response|
164
+ @logger.info("Waiting for stopping all instances...#{attempts}")
165
+ instances = response.reservations.flat_map(&:instances)
166
+ instances.sort_by(&:instance_id).each do |instance|
167
+ @logger.info("#{instance.instance_id}\t#{instance.state.name}")
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -1,3 +1,4 @@
1
+ require 'aws-sdk-cloudwatchevents'
1
2
  require 'timeout'
2
3
 
3
4
  module EcsDeploy
@@ -8,7 +9,7 @@ module EcsDeploy
8
9
 
9
10
  def initialize(
10
11
  cluster:, rule_name:, schedule_expression:, enabled: true, description: nil, target_id: nil,
11
- task_definition_name:, revision: nil, task_count: nil, role_arn:,
12
+ task_definition_name:, revision: nil, task_count: nil, role_arn:, network_configuration: nil, launch_type: nil, platform_version: nil, group: nil,
12
13
  region: nil, container_overrides: nil
13
14
  )
14
15
  @cluster = cluster
@@ -21,10 +22,15 @@ module EcsDeploy
21
22
  @task_count = task_count || 1
22
23
  @revision = revision
23
24
  @role_arn = role_arn
24
- @region = region || EcsDeploy.config.default_region || ENV["AWS_DEFAULT_REGION"]
25
+ @network_configuration = network_configuration
26
+ @launch_type = launch_type || "EC2"
27
+ @platform_version = platform_version
28
+ @group = group
29
+ region ||= EcsDeploy.config.default_region
25
30
  @container_overrides = container_overrides
26
31
 
27
- @client = Aws::ECS::Client.new(region: @region)
32
+ @client = region ? Aws::ECS::Client.new(region: region) : Aws::ECS::Client.new
33
+ @region = @client.config.region
28
34
  @cloud_watch_events = Aws::CloudWatchEvents::Client.new(region: @region)
29
35
  end
30
36
 
@@ -66,8 +72,14 @@ module EcsDeploy
66
72
  ecs_parameters: {
67
73
  task_definition_arn: task_definition_arn,
68
74
  task_count: @task_count,
75
+ network_configuration: @network_configuration,
76
+ launch_type: @launch_type,
77
+ platform_version: @platform_version,
78
+ group: @group,
69
79
  },
70
80
  }
81
+ target[:ecs_parameters].compact!
82
+
71
83
  if @container_overrides
72
84
  target.merge!(input: { containerOverrides: @container_overrides }.to_json)
73
85
  end
@@ -5,13 +5,23 @@ module EcsDeploy
5
5
  CHECK_INTERVAL = 5
6
6
  MAX_DESCRIBE_SERVICES = 10
7
7
 
8
- attr_reader :cluster, :region, :service_name
8
+ attr_reader :cluster, :region, :service_name, :delete
9
9
 
10
10
  def initialize(
11
11
  cluster:, service_name:, task_definition_name: nil, revision: nil,
12
12
  load_balancers: nil,
13
13
  desired_count: nil, deployment_configuration: {maximum_percent: 200, minimum_healthy_percent: 100},
14
- region: nil
14
+ launch_type: nil,
15
+ placement_constraints: [],
16
+ placement_strategy: [],
17
+ network_configuration: nil,
18
+ health_check_grace_period_seconds: nil,
19
+ scheduling_strategy: 'REPLICA',
20
+ enable_ecs_managed_tags: nil,
21
+ tags: nil,
22
+ propagate_tags: nil,
23
+ region: nil,
24
+ delete: false
15
25
  )
16
26
  @cluster = cluster
17
27
  @service_name = service_name
@@ -19,11 +29,24 @@ module EcsDeploy
19
29
  @load_balancers = load_balancers
20
30
  @desired_count = desired_count
21
31
  @deployment_configuration = deployment_configuration
32
+ @launch_type = launch_type
33
+ @placement_constraints = placement_constraints
34
+ @placement_strategy = placement_strategy
35
+ @network_configuration = network_configuration
36
+ @health_check_grace_period_seconds = health_check_grace_period_seconds
37
+ @scheduling_strategy = scheduling_strategy
22
38
  @revision = revision
23
- @region = region || EcsDeploy.config.default_region || ENV["AWS_DEFAULT_REGION"]
39
+ @enable_ecs_managed_tags = enable_ecs_managed_tags
40
+ @tags = tags
41
+ @propagate_tags = propagate_tags
42
+
24
43
  @response = nil
25
44
 
26
- @client = Aws::ECS::Client.new(region: @region)
45
+ region ||= EcsDeploy.config.default_region
46
+ @client = region ? Aws::ECS::Client.new(region: region) : Aws::ECS::Client.new
47
+ @region = @client.config.region
48
+
49
+ @delete = delete
27
50
  end
28
51
 
29
52
  def current_task_definition_arn
@@ -37,35 +60,91 @@ module EcsDeploy
37
60
  cluster: @cluster,
38
61
  task_definition: task_definition_name_with_revision,
39
62
  deployment_configuration: @deployment_configuration,
63
+ network_configuration: @network_configuration,
64
+ health_check_grace_period_seconds: @health_check_grace_period_seconds,
40
65
  }
41
66
  if res.services.select{ |s| s.status == 'ACTIVE' }.empty?
67
+ return if @delete
68
+
42
69
  service_options.merge!({
43
70
  service_name: @service_name,
44
71
  desired_count: @desired_count.to_i,
72
+ launch_type: @launch_type,
73
+ placement_constraints: @placement_constraints,
74
+ placement_strategy: @placement_strategy,
75
+ enable_ecs_managed_tags: @enable_ecs_managed_tags,
76
+ tags: @tags,
77
+ propagate_tags: @propagate_tags,
45
78
  })
46
- if @load_balancers
79
+
80
+ if @load_balancers && EcsDeploy.config.ecs_service_role
47
81
  service_options.merge!({
48
82
  role: EcsDeploy.config.ecs_service_role,
83
+ })
84
+ end
85
+
86
+ if @load_balancers
87
+ service_options.merge!({
49
88
  load_balancers: @load_balancers,
50
89
  })
51
90
  end
91
+
92
+ if @scheduling_strategy == 'DAEMON'
93
+ service_options[:scheduling_strategy] = @scheduling_strategy
94
+ service_options.delete(:desired_count)
95
+ end
52
96
  @response = @client.create_service(service_options)
53
97
  EcsDeploy.logger.info "create service [#{@service_name}] [#{@region}] [#{Paint['OK', :green]}]"
54
98
  else
99
+ return delete_service if @delete
100
+
55
101
  service_options.merge!({service: @service_name})
56
102
  service_options.merge!({desired_count: @desired_count}) if @desired_count
103
+ update_tags(@service_name, @tags)
57
104
  @response = @client.update_service(service_options)
58
105
  EcsDeploy.logger.info "update service [#{@service_name}] [#{@region}] [#{Paint['OK', :green]}]"
59
106
  end
60
107
  end
61
108
 
109
+ def delete_service
110
+ if @scheduling_strategy != 'DAEMON'
111
+ @client.update_service(cluster: @cluster, service: @service_name, desired_count: 0)
112
+ sleep 1
113
+ end
114
+ @client.delete_service(cluster: @cluster, service: @service_name)
115
+ EcsDeploy.logger.info "delete service [#{@service_name}] [#{@region}] [#{Paint['OK', :green]}]"
116
+ end
117
+
118
+ def update_tags(service_name, tags)
119
+ service_arn = @client.describe_services(cluster: @cluster, services: [service_name]).services.first.service_arn
120
+ if service_arn.split('/').size == 2
121
+ if tags
122
+ EcsDeploy.logger.warn "#{service_name} doesn't support tagging operations, so tags are ignored. Long arn format must be used for tagging operations."
123
+ end
124
+ return
125
+ end
126
+
127
+ tags ||= []
128
+ current_tag_keys = @client.list_tags_for_resource(resource_arn: service_arn).tags.map(&:key)
129
+ deleted_tag_keys = current_tag_keys - tags.map { |t| t[:key] }
130
+
131
+ unless deleted_tag_keys.empty?
132
+ @client.untag_resource(resource_arn: service_arn, tag_keys: deleted_tag_keys)
133
+ end
134
+
135
+ unless tags.empty?
136
+ @client.tag_resource(resource_arn: service_arn, tags: tags)
137
+ end
138
+ end
139
+
62
140
  def wait_running
63
141
  return if @response.nil?
64
142
 
65
143
  service = @response.service
66
144
 
67
145
  @client.wait_until(:services_stable, cluster: @cluster, services: [service.service_name]) do |w|
68
- w.delay = 10
146
+ w.delay = EcsDeploy.config.ecs_wait_until_services_stable_delay if EcsDeploy.config.ecs_wait_until_services_stable_delay
147
+ w.max_attempts = EcsDeploy.config.ecs_wait_until_services_stable_max_attempts if EcsDeploy.config.ecs_wait_until_services_stable_max_attempts
69
148
 
70
149
  w.before_attempt do
71
150
  EcsDeploy.logger.info "wait service stable [#{service.service_name}]"
@@ -76,8 +155,11 @@ module EcsDeploy
76
155
  def self.wait_all_running(services)
77
156
  services.group_by { |s| [s.cluster, s.region] }.each do |(cl, region), ss|
78
157
  client = Aws::ECS::Client.new(region: region)
79
- ss.map(&:service_name).each_slice(MAX_DESCRIBE_SERVICES) do |chunked_service_names|
158
+ ss.reject(&:delete).map(&:service_name).each_slice(MAX_DESCRIBE_SERVICES) do |chunked_service_names|
80
159
  client.wait_until(:services_stable, cluster: cl, services: chunked_service_names) do |w|
160
+ w.delay = EcsDeploy.config.ecs_wait_until_services_stable_delay if EcsDeploy.config.ecs_wait_until_services_stable_delay
161
+ w.max_attempts = EcsDeploy.config.ecs_wait_until_services_stable_max_attempts if EcsDeploy.config.ecs_wait_until_services_stable_max_attempts
162
+
81
163
  w.before_attempt do
82
164
  EcsDeploy.logger.info "wait service stable [#{chunked_service_names.join(", ")}]"
83
165
  end
@@ -1,37 +1,47 @@
1
1
  module EcsDeploy
2
2
  class TaskDefinition
3
3
  def self.deregister(arn, region: nil)
4
- region = region || EcsDeploy.config.default_region || ENV["AWS_DEFAULT_REGION"]
5
- client = Aws::ECS::Client.new(region: region)
4
+ region ||= EcsDeploy.config.default_region
5
+ client = region ? Aws::ECS::Client.new(region: region) : Aws::ECS::Client.new
6
6
  client.deregister_task_definition({
7
7
  task_definition: arn,
8
8
  })
9
- EcsDeploy.logger.info "deregister task definition [#{arn}] [#{region}] [#{Paint['OK', :green]}]"
9
+ EcsDeploy.logger.info "deregister task definition [#{arn}] [#{client.config.region}] [#{Paint['OK', :green]}]"
10
10
  end
11
11
 
12
12
  def initialize(
13
13
  task_definition_name:, region: nil,
14
14
  network_mode: "bridge", volumes: [], container_definitions: [], placement_constraints: [],
15
- task_role_arn: nil
15
+ task_role_arn: nil,
16
+ execution_role_arn: nil,
17
+ requires_compatibilities: nil,
18
+ cpu: nil, memory: nil,
19
+ tags: nil
16
20
  )
17
21
  @task_definition_name = task_definition_name
18
22
  @task_role_arn = task_role_arn
19
- @region = region || EcsDeploy.config.default_region || ENV["AWS_DEFAULT_REGION"]
23
+ @execution_role_arn = execution_role_arn
24
+ region ||= EcsDeploy.config.default_region
20
25
 
21
26
  @container_definitions = container_definitions.map do |cd|
22
27
  if cd[:docker_labels]
23
28
  cd[:docker_labels] = cd[:docker_labels].map { |k, v| [k.to_s, v] }.to_h
24
29
  end
25
- if cd[:log_configuration] && cd[:log_configuration][:options]
26
- cd[:log_configuration][:options] = cd[:log_configuration][:options].map { |k, v| [k.to_s, v] }.to_h
30
+ if cd.dig(:log_configuration, :options)
31
+ cd[:log_configuration][:options] = cd.dig(:log_configuration, :options).map { |k, v| [k.to_s, v] }.to_h
27
32
  end
28
33
  cd
29
34
  end
30
35
  @volumes = volumes
31
36
  @network_mode = network_mode
32
37
  @placement_constraints = placement_constraints
38
+ @requires_compatibilities = requires_compatibilities
39
+ @cpu = cpu&.to_s
40
+ @memory = memory&.to_s
41
+ @tags = tags
33
42
 
34
- @client = Aws::ECS::Client.new(region: @region)
43
+ @client = region ? Aws::ECS::Client.new(region: region) : Aws::ECS::Client.new
44
+ @region = @client.config.region
35
45
  end
36
46
 
37
47
  def recent_task_definition_arns
@@ -52,6 +62,10 @@ module EcsDeploy
52
62
  volumes: @volumes,
53
63
  placement_constraints: @placement_constraints,
54
64
  task_role_arn: @task_role_arn,
65
+ execution_role_arn: @execution_role_arn,
66
+ requires_compatibilities: @requires_compatibilities,
67
+ cpu: @cpu, memory: @memory,
68
+ tags: @tags
55
69
  })
56
70
  EcsDeploy.logger.info "register task definition [#{@task_definition_name}] [#{@region}] [#{Paint['OK', :green]}]"
57
71
  res.task_definition