hako 2.2.0 → 2.3.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.
@@ -13,6 +13,7 @@ require 'hako/schedulers/ecs_definition_comparator'
13
13
  require 'hako/schedulers/ecs_elb'
14
14
  require 'hako/schedulers/ecs_elb_v2'
15
15
  require 'hako/schedulers/ecs_service_comparator'
16
+ require 'hako/schedulers/ecs_volume_comparator'
16
17
 
17
18
  module Hako
18
19
  module Schedulers
@@ -45,7 +46,7 @@ module Hako
45
46
  @dynamic_port_mapping = options.fetch('dynamic_port_mapping', @ecs_elb_options.nil?)
46
47
  @health_check_grace_period_seconds = options.fetch('health_check_grace_period_seconds', nil)
47
48
  if options.key?('autoscaling')
48
- @autoscaling = EcsAutoscaling.new(options.fetch('autoscaling'), @region, dry_run: @dry_run)
49
+ @autoscaling = EcsAutoscaling.new(options.fetch('autoscaling'), @region, ecs_elb_client, dry_run: @dry_run)
49
50
  end
50
51
  @autoscaling_group_for_oneshot = options.fetch('autoscaling_group_for_oneshot', nil)
51
52
  @autoscaling_topic_for_oneshot = options.fetch('autoscaling_topic_for_oneshot', nil)
@@ -94,6 +95,9 @@ module Hako
94
95
  definitions = create_definitions(containers)
95
96
 
96
97
  if @dry_run
98
+ volumes_definition.each do |d|
99
+ print_volume_definition_in_cli_format(d)
100
+ end
97
101
  definitions.each do |d|
98
102
  print_definition_in_cli_format(d)
99
103
  end
@@ -176,6 +180,9 @@ module Hako
176
180
  end
177
181
 
178
182
  if @dry_run
183
+ volumes_definition.each do |d|
184
+ print_volume_definition_in_cli_format(d)
185
+ end
179
186
  definitions.each do |d|
180
187
  if d[:name] == 'app'
181
188
  d[:command] = commands
@@ -405,6 +412,10 @@ module Hako
405
412
  # Initial deployment
406
413
  return true
407
414
  end
415
+ actual_volume_definitions = {}
416
+ actual_definition.volumes.each do |v|
417
+ actual_volume_definitions[v.name] = v
418
+ end
408
419
  container_definitions = {}
409
420
  actual_definition.container_definitions.each do |c|
410
421
  container_definitions[c.name] = c
@@ -413,7 +424,10 @@ module Hako
413
424
  if actual_definition.task_role_arn != @task_role_arn
414
425
  return true
415
426
  end
416
- if different_volumes?(actual_definition.volumes)
427
+ if volumes_definition.any? { |definition| different_volume?(definition, actual_volume_definitions.delete(definition[:name])) }
428
+ return true
429
+ end
430
+ unless actual_volume_definitions.empty?
417
431
  return true
418
432
  end
419
433
  if desired_definitions.any? { |definition| different_definition?(definition, container_definitions.delete(definition[:name])) }
@@ -441,23 +455,11 @@ module Hako
441
455
  false
442
456
  end
443
457
 
444
- # @param [Hash<String, Hash<String, String>>] actual_volumes
458
+ # @param [Hash] expected_volume
459
+ # @param [Aws::ECS::Types::Volume] actual_volume
445
460
  # @return [Boolean]
446
- def different_volumes?(actual_volumes)
447
- if @volumes.size != actual_volumes.size
448
- return true
449
- end
450
- actual_volumes.each do |actual_volume|
451
- expected_volume = @volumes[actual_volume.name]
452
- if expected_volume.nil?
453
- return true
454
- end
455
- if expected_volume['source_path'] != actual_volume.host.source_path
456
- return true
457
- end
458
- end
459
-
460
- false
461
+ def different_volume?(expected_volume, actual_volume)
462
+ EcsVolumeComparator.new(expected_volume).different?(actual_volume)
461
463
  end
462
464
 
463
465
  # @param [Hash] expected_container
@@ -534,13 +536,28 @@ module Hako
534
536
  raise Error.new('Unable to register task definition for oneshot due to too many client errors')
535
537
  end
536
538
 
537
- # @return [Hash]
539
+ # @return [Array<Hash>]
538
540
  def volumes_definition
539
- @volumes.map do |name, volume|
540
- {
541
- name: name,
542
- host: { source_path: volume['source_path'] },
543
- }
541
+ @volumes_definition ||= @volumes.map do |name, volume|
542
+ definition = { name: name }
543
+ if volume.key?('docker_volume_configuration')
544
+ configuration = volume['docker_volume_configuration']
545
+ definition[:docker_volume_configuration] = {
546
+ autoprovision: configuration['autoprovision'],
547
+ driver: configuration['driver'],
548
+ # ECS API doesn't allow 'driver_opts' to be an empty hash.
549
+ driver_opts: configuration['driver_opts'],
550
+ # ECS API doesn't allow 'labels' to be an empty hash.
551
+ labels: configuration['labels'],
552
+ scope: configuration['scope'],
553
+ }
554
+ else
555
+ # When neither 'host' nor 'docker_volume_configuration' is
556
+ # specified, ECS API treats it as if 'host' is specified without
557
+ # 'source_path'.
558
+ definition[:host] = { source_path: volume['source_path'] }
559
+ end
560
+ definition
544
561
  end
545
562
  end
546
563
 
@@ -574,6 +591,7 @@ module Hako
574
591
  volumes_from: container.volumes_from,
575
592
  user: container.user,
576
593
  log_configuration: container.log_configuration,
594
+ health_check: container.health_check,
577
595
  ulimits: container.ulimits,
578
596
  extra_hosts: container.extra_hosts,
579
597
  }
@@ -1043,6 +1061,31 @@ module Hako
1043
1061
  end
1044
1062
  end
1045
1063
 
1064
+ # @param [Hash] definition
1065
+ # @return [nil]
1066
+ def print_volume_definition_in_cli_format(definition)
1067
+ return if definition.dig(:docker_volume_configuration, :autoprovision)
1068
+ # From version 1.20.0 of ECS agent, a local volume is provisioned when
1069
+ # 'host' is specified without 'source_path'.
1070
+ return if definition.dig(:host, :source_path)
1071
+
1072
+ cmd = %w[docker volume create]
1073
+ if (configuration = definition[:docker_volume_configuration])
1074
+ if configuration[:driver]
1075
+ cmd << '--driver' << configuration[:driver]
1076
+ end
1077
+ (configuration[:driver_opts] || {}).each do |k, v|
1078
+ cmd << '--opt' << "#{k}=#{v}"
1079
+ end
1080
+ (configuration[:labels] || {}).each do |k, v|
1081
+ cmd << '--label' << "#{k}=#{v}"
1082
+ end
1083
+ end
1084
+ cmd << definition[:name]
1085
+ puts cmd.join(' ')
1086
+ nil
1087
+ end
1088
+
1046
1089
  # @param [Hash] definition
1047
1090
  # @param [Hash<String, String>] additional_env
1048
1091
  # @return [nil]
@@ -1069,12 +1112,13 @@ module Hako
1069
1112
  end
1070
1113
  definition.fetch(:mount_points).each do |mount_point|
1071
1114
  source_volume = mount_point.fetch(:source_volume)
1072
- v = @volumes[source_volume]
1073
- if v
1074
- cmd << '--volume' << "#{v.fetch('source_path')}:#{mount_point.fetch(:container_path)}#{mount_point[:read_only] ? ':ro' : ''}"
1075
- else
1076
- raise "Could not find volume #{source_volume}"
1077
- end
1115
+ v = volumes_definition.find { |d| d[:name] == source_volume }
1116
+ raise "Could not find volume #{source_volume}" unless v
1117
+ source = v.dig(:host, :source_path) || source_volume
1118
+ cmd << '--volume' << "#{source}:#{mount_point.fetch(:container_path)}#{mount_point[:read_only] ? ':ro' : ''}"
1119
+ end
1120
+ definition.fetch(:volumes_from).each do |volumes_from|
1121
+ cmd << '--volumes-from' << "#{volumes_from.fetch(:source_container)}#{volumes_from[:read_only] ? ':ro' : ''}"
1078
1122
  end
1079
1123
  if definition[:privileged]
1080
1124
  cmd << '--privileged'
@@ -1103,9 +1147,18 @@ module Hako
1103
1147
  end
1104
1148
  end
1105
1149
 
1106
- if definition[:init_process_enabled]
1150
+ if definition[:linux_parameters][:init_process_enabled]
1107
1151
  cmd << '--init'
1108
1152
  end
1153
+
1154
+ if definition[:linux_parameters][:shared_memory_size]
1155
+ cmd << '--shm-size' << "#{definition[:linux_parameters][:shared_memory_size]}m"
1156
+ end
1157
+
1158
+ definition[:linux_parameters].fetch(:tmpfs, []).each do |tmpfs|
1159
+ options = ["size=#{tmpfs[:size]}m"].concat(tmpfs[:mount_options])
1160
+ cmd << '--tmpfs' << "#{tmpfs[:container_path]}:#{options.join(',')}"
1161
+ end
1109
1162
  end
1110
1163
  definition.fetch(:volumes_from).each do |volumes_from|
1111
1164
  p volumes_from
@@ -2,14 +2,16 @@
2
2
 
3
3
  require 'aws-sdk-applicationautoscaling'
4
4
  require 'aws-sdk-cloudwatch'
5
+ require 'aws-sdk-elasticloadbalancingv2'
5
6
  require 'hako'
6
7
  require 'hako/error'
7
8
 
8
9
  module Hako
9
10
  module Schedulers
10
11
  class EcsAutoscaling
11
- def initialize(options, region, dry_run:)
12
+ def initialize(options, region, ecs_elb_client, dry_run:)
12
13
  @region = region
14
+ @ecs_elb_client = ecs_elb_client
13
15
  @dry_run = dry_run
14
16
  @role_arn = required_option(options, 'role_arn')
15
17
  @min_capacity = required_option(options, 'min_capacity')
@@ -44,17 +46,21 @@ module Hako
44
46
  @policies.each do |policy|
45
47
  Hako.logger.info("Configuring scaling policy #{policy.name}")
46
48
  if @dry_run
47
- policy.alarms.each do |alarm_name|
48
- Hako.logger.info("Configuring #{alarm_name}'s alarm_action")
49
+ if policy.policy_type == 'StepScaling'
50
+ policy.alarms.each do |alarm_name|
51
+ Hako.logger.info("Configuring #{alarm_name}'s alarm_action")
52
+ end
49
53
  end
50
54
  else
51
- policy_arn = autoscaling_client.put_scaling_policy(
55
+ policy_params = {
52
56
  policy_name: policy.name,
53
57
  service_namespace: service_namespace,
54
58
  resource_id: resource_id,
55
59
  scalable_dimension: scalable_dimension,
56
- policy_type: 'StepScaling',
57
- step_scaling_policy_configuration: {
60
+ policy_type: policy.policy_type,
61
+ }
62
+ if policy.policy_type == 'StepScaling'
63
+ policy_params[:step_scaling_policy_configuration] = {
58
64
  adjustment_type: policy.adjustment_type,
59
65
  step_adjustments: [
60
66
  {
@@ -65,16 +71,40 @@ module Hako
65
71
  ],
66
72
  cooldown: policy.cooldown,
67
73
  metric_aggregation_type: policy.metric_aggregation_type,
68
- },
69
- ).policy_arn
70
-
71
- alarms = cw_client.describe_alarms(alarm_names: policy.alarms).flat_map(&:metric_alarms).map { |a| [a.alarm_name, a] }.to_h
72
- policy.alarms.each do |alarm_name|
73
- alarm = alarms.fetch(alarm_name) { raise Error.new("Alarm #{alarm_name} does not exist") }
74
- Hako.logger.info("Updating #{alarm_name}'s alarm_actions from #{alarm.alarm_actions} to #{[policy_arn]}")
75
- params = PUT_METRIC_ALARM_OPTIONS.map { |key| [key, alarm.public_send(key)] }.to_h
76
- params[:alarm_actions] = [policy_arn]
77
- cw_client.put_metric_alarm(params)
74
+ }
75
+ else
76
+ predefined_metric_specification = {
77
+ predefined_metric_type: policy.predefined_metric_type,
78
+ }
79
+ if policy.predefined_metric_type == 'ALBRequestCountPerTarget'
80
+ if service.load_balancers.empty? || service.load_balancers[0].target_group_arn.nil?
81
+ raise Error.new('Target group must be attached to the ECS service for predefined metric type ALBRequestCountPerTarget')
82
+ end
83
+ resource_label = target_group_resource_label
84
+ unless resource_label.start_with?('app/')
85
+ raise Error.new("Load balancer type must be 'application' for predefined metric type ALBRequestCountPerTarget")
86
+ end
87
+ predefined_metric_specification[:resource_label] = resource_label
88
+ end
89
+ policy_params[:target_tracking_scaling_policy_configuration] = {
90
+ target_value: policy.target_value,
91
+ predefined_metric_specification: predefined_metric_specification,
92
+ scale_out_cooldown: policy.scale_out_cooldown,
93
+ scale_in_cooldown: policy.scale_in_cooldown,
94
+ disable_scale_in: policy.disable_scale_in,
95
+ }
96
+ end
97
+ policy_arn = autoscaling_client.put_scaling_policy(policy_params).policy_arn
98
+
99
+ if policy.policy_type == 'StepScaling'
100
+ alarms = cw_client.describe_alarms(alarm_names: policy.alarms).flat_map(&:metric_alarms).map { |a| [a.alarm_name, a] }.to_h
101
+ policy.alarms.each do |alarm_name|
102
+ alarm = alarms.fetch(alarm_name) { raise Error.new("Alarm #{alarm_name} does not exist") }
103
+ Hako.logger.info("Updating #{alarm_name}'s alarm_actions from #{alarm.alarm_actions} to #{[policy_arn]}")
104
+ params = PUT_METRIC_ALARM_OPTIONS.map { |key| [key, alarm.public_send(key)] }.to_h
105
+ params[:alarm_actions] = [policy_arn]
106
+ cw_client.put_metric_alarm(params)
107
+ end
78
108
  end
79
109
  end
80
110
  end
@@ -139,23 +169,50 @@ module Hako
139
169
  "service/#{service.cluster_arn.slice(%r{[^/]+\z}, 0)}/#{service.service_name}"
140
170
  end
141
171
 
172
+ # @return [String]
173
+ def target_group_resource_label
174
+ target_group = @ecs_elb_client.describe_target_group
175
+ load_balancer_arn = target_group.load_balancer_arns[0]
176
+ target_group_arn = target_group.target_group_arn
177
+ "#{load_balancer_arn.slice(%r{:loadbalancer/(.+)\z}, 1)}/#{target_group_arn.slice(/[^:]+\z/)}"
178
+ end
179
+
142
180
  class Policy
181
+ attr_reader :policy_type
143
182
  attr_reader :alarms, :cooldown, :adjustment_type, :scaling_adjustment, :metric_interval_lower_bound, :metric_interval_upper_bound, :metric_aggregation_type
183
+ attr_reader :target_value, :predefined_metric_type, :scale_out_cooldown, :scale_in_cooldown, :disable_scale_in
144
184
 
145
185
  # @param [Hash] options
146
186
  def initialize(options)
147
- @alarms = required_option(options, 'alarms')
148
- @cooldown = required_option(options, 'cooldown')
149
- @adjustment_type = required_option(options, 'adjustment_type')
150
- @scaling_adjustment = required_option(options, 'scaling_adjustment')
151
- @metric_interval_lower_bound = options.fetch('metric_interval_lower_bound', nil)
152
- @metric_interval_upper_bound = options.fetch('metric_interval_upper_bound', nil)
153
- @metric_aggregation_type = required_option(options, 'metric_aggregation_type')
187
+ @policy_type = options.fetch('policy_type', 'StepScaling')
188
+ case @policy_type
189
+ when 'StepScaling'
190
+ @alarms = required_option(options, 'alarms')
191
+ @cooldown = required_option(options, 'cooldown')
192
+ @adjustment_type = required_option(options, 'adjustment_type')
193
+ @scaling_adjustment = required_option(options, 'scaling_adjustment')
194
+ @metric_interval_lower_bound = options.fetch('metric_interval_lower_bound', nil)
195
+ @metric_interval_upper_bound = options.fetch('metric_interval_upper_bound', nil)
196
+ @metric_aggregation_type = required_option(options, 'metric_aggregation_type')
197
+ when 'TargetTrackingScaling'
198
+ @name = required_option(options, 'name')
199
+ @target_value = required_option(options, 'target_value')
200
+ @predefined_metric_type = required_option(options, 'predefined_metric_type')
201
+ @scale_out_cooldown = options.fetch('scale_out_cooldown', nil)
202
+ @scale_in_cooldown = options.fetch('scale_in_cooldown', nil)
203
+ @disable_scale_in = options.fetch('disable_scale_in', nil)
204
+ else
205
+ raise Error.new("scheduler.autoscaling.policies.#{policy_type} must be either 'StepScaling' or 'TargetTrackingScaling'")
206
+ end
154
207
  end
155
208
 
156
209
  # @return [String]
157
210
  def name
158
- alarms.join('-and-')
211
+ if policy_type == 'StepScaling'
212
+ alarms.join('-and-')
213
+ else
214
+ @name
215
+ end
159
216
  end
160
217
 
161
218
  private
@@ -35,6 +35,7 @@ module Hako
35
35
  struct.member(:user, Schema::Nullable.new(Schema::String.new))
36
36
  struct.member(:privileged, Schema::Boolean.new)
37
37
  struct.member(:log_configuration, Schema::Nullable.new(log_configuration_schema))
38
+ struct.member(:health_check, Schema::Nullable.new(health_check_schema))
38
39
  struct.member(:ulimits, Schema::Nullable.new(ulimits_schema))
39
40
  struct.member(:extra_hosts, Schema::Nullable.new(extra_hosts_schema))
40
41
  struct.member(:linux_parameters, Schema::Nullable.new(linux_parameters_schema))
@@ -78,6 +79,16 @@ module Hako
78
79
  end
79
80
  end
80
81
 
82
+ def health_check_schema
83
+ Schema::Structure.new.tap do |struct|
84
+ struct.member(:command, Schema::OrderedArray.new(Schema::String.new))
85
+ struct.member(:interval, Schema::Integer.new)
86
+ struct.member(:timeout, Schema::Integer.new)
87
+ struct.member(:retries, Schema::Integer.new)
88
+ struct.member(:start_period, Schema::Integer.new)
89
+ end
90
+ end
91
+
81
92
  def ulimits_schema
82
93
  Schema::UnorderedArray.new(ulimit_schema)
83
94
  end
@@ -95,6 +106,8 @@ module Hako
95
106
  struct.member(:capabilities, Schema::Nullable.new(capabilities_schema))
96
107
  struct.member(:devices, Schema::Nullable.new(devices_schema))
97
108
  struct.member(:init_process_enabled, Schema::Nullable.new(Schema::Boolean.new))
109
+ struct.member(:shared_memory_size, Schema::Nullable.new(Schema::Integer.new))
110
+ struct.member(:tmpfs, Schema::Nullable.new(tmpfs_schema))
98
111
  end
99
112
  end
100
113
 
@@ -117,6 +130,16 @@ module Hako
117
130
  end
118
131
  end
119
132
 
133
+ def tmpfs_schema
134
+ Schema::UnorderedArray.new(
135
+ Schema::Structure.new.tap do |struct|
136
+ struct.member(:container_path, Schema::String.new)
137
+ struct.member(:mount_options, Schema::UnorderedArray.new(Schema::String.new))
138
+ struct.member(:size, Schema::Integer.new)
139
+ end
140
+ )
141
+ end
142
+
120
143
  def extra_hosts_schema
121
144
  Schema::UnorderedArray.new(extra_host_schema)
122
145
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hako/schema'
4
+
5
+ module Hako
6
+ module Schedulers
7
+ class EcsVolumeComparator
8
+ # @param [Hash] expected_volume
9
+ def initialize(expected_volume)
10
+ @expected_volume = expected_volume
11
+ @schema = volume_schema
12
+ end
13
+
14
+ # @param [Aws::ECS::Types::Volume] actual_volume
15
+ # @return [Boolean]
16
+ def different?(actual_volume)
17
+ !@schema.same?(actual_volume.to_h, @expected_volume)
18
+ end
19
+
20
+ private
21
+
22
+ def volume_schema
23
+ Schema::Structure.new.tap do |struct|
24
+ struct.member(:docker_volume_configuration, Schema::Nullable.new(docker_volume_configuration_schema))
25
+ struct.member(:host, Schema::Nullable.new(host_schema))
26
+ struct.member(:name, Schema::String.new)
27
+ end
28
+ end
29
+
30
+ def docker_volume_configuration_schema
31
+ Schema::Structure.new.tap do |struct|
32
+ struct.member(:autoprovision, Schema::Nullable.new(Schema::Boolean.new))
33
+ struct.member(:driver, Schema::WithDefault.new(Schema::String.new, 'local'))
34
+ struct.member(:driver_opts, Schema::Nullable.new(Schema::Table.new(Schema::String.new, Schema::String.new)))
35
+ struct.member(:labels, Schema::Nullable.new(Schema::Table.new(Schema::String.new, Schema::String.new)))
36
+ struct.member(:scope, Schema::WithDefault.new(Schema::String.new, 'task'))
37
+ end
38
+ end
39
+
40
+ def host_schema
41
+ Schema::Structure.new.tap do |struct|
42
+ struct.member(:source_path, Schema::Nullable.new(Schema::String.new))
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
data/lib/hako/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hako
4
- VERSION = '2.2.0'
4
+ VERSION = '2.3.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hako
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kohei Suzuki
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-06-29 00:00:00.000000000 Z
11
+ date: 2018-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-applicationautoscaling
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: 1.4.0
89
+ version: 1.7.0
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
- version: 1.4.0
96
+ version: 1.7.0
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: aws-sdk-elasticloadbalancing
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -287,7 +287,6 @@ files:
287
287
  - docs/jsonnet.md
288
288
  - examples/create_aws_cloud_watch_logs_log_group.libsonnet
289
289
  - examples/front.libsonnet
290
- - examples/front.yml
291
290
  - examples/hello-autoscaling-group.jsonnet
292
291
  - examples/hello-autoscaling.jsonnet
293
292
  - examples/hello-awslogs-driver.jsonnet
@@ -328,6 +327,7 @@ files:
328
327
  - lib/hako/schedulers/ecs_elb.rb
329
328
  - lib/hako/schedulers/ecs_elb_v2.rb
330
329
  - lib/hako/schedulers/ecs_service_comparator.rb
330
+ - lib/hako/schedulers/ecs_volume_comparator.rb
331
331
  - lib/hako/schema.rb
332
332
  - lib/hako/schema/boolean.rb
333
333
  - lib/hako/schema/integer.rb
data/examples/front.yml DELETED
@@ -1,5 +0,0 @@
1
- type: nginx_front
2
- s3:
3
- region: ap-northeast-1
4
- bucket: nanika
5
- prefix: hako/front_config