hako 2.6.2 → 2.7.0

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: b17b6419ac68f7381855d71a4a8bb268ab1f453e148c55f655704a9d016de402
4
- data.tar.gz: 4263e33011c50dac954f497a8c10744270a7e0042c25ce0550efb752114b4231
3
+ metadata.gz: 5e976128af20ee669b05e2e9531dc84d3b0ad46dcba305fa732cfbf0e24215dc
4
+ data.tar.gz: 0cde6cd9e06468444bd63a55f869424b3daae0926d6b8cd9a049e5511ba8ef30
5
5
  SHA512:
6
- metadata.gz: 5c278d415a8d1bcf4c1e29346f8f298b5de39175cd5b02a7233230537881be87b7445ad4b329f1903ae9912d6f8408e7bca41d367773888323995f46977311ba
7
- data.tar.gz: d44cc04c09be159f7b179f82a5b17c93fb7cd88d2c562567532b64eec72429a0625b0445769362f52802d81a7a2527d609c3ace2d1f2c325078b6a97f1e317a5
6
+ metadata.gz: 27279574a6d795bbe859463056f7460b501e3d2a0c38f6fa147aeafb419970ec9a02efed532fd35497f040a5fcb758145d224e6611bcc68cc003e586dc101753
7
+ data.tar.gz: ebb6948d88f10ff6f8ad3e7dbc3f47ea674cfb45d35b391c6611de9cbdb87bd73ea8cca02112f2dc4e27b6798a9e0308bb437aaa5db2586e2d2e747e7363ad8e
@@ -1,3 +1,9 @@
1
+ # 2.7.0 (2019-03-15)
2
+ ## New features
3
+ - Support `entry_point` parameter
4
+ - Support ECS Service Discovery
5
+ - See [examples/hello-service-discovery.jsonnet](examples/hello-service-discovery.jsonnet)
6
+
1
7
  # 2.6.2 (2018-12-19)
2
8
  ## Bug fixes
3
9
  - Set `platform_version` correctly
@@ -0,0 +1,49 @@
1
+ local fileProvider = std.native('provide.file');
2
+ local provide(name) = fileProvider(std.toString({ path: 'hello.env' }), name);
3
+
4
+ {
5
+ scheduler: {
6
+ type: 'ecs',
7
+ region: 'ap-northeast-1',
8
+ cluster: 'eagletmt',
9
+ desired_count: 2,
10
+ role: 'ecsServiceRole',
11
+ service_discovery: [
12
+ {
13
+ container_name: 'app',
14
+ container_port: 80,
15
+ service: {
16
+ name: 'hello-service-discovery',
17
+ namespace_id: 'ns-XXXXXXXXXXXXXXXX',
18
+ dns_config: {
19
+ dns_records: [
20
+ {
21
+ type: 'SRV',
22
+ ttl: 60,
23
+ },
24
+ ],
25
+ },
26
+ health_check_custom_config: {
27
+ failure_threshold: 1,
28
+ },
29
+ },
30
+ },
31
+ ],
32
+ },
33
+ app: {
34
+ image: 'ryotarai/hello-sinatra',
35
+ memory: 128,
36
+ cpu: 256,
37
+ env: {
38
+ PORT: '3000',
39
+ MESSAGE: std.format('%s-san', provide('username')),
40
+ },
41
+ port_mappings: [
42
+ {
43
+ container_port: 3000,
44
+ host_port: 0,
45
+ protocol: 'tcp',
46
+ },
47
+ ],
48
+ },
49
+ }
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_dependency 'aws-sdk-elasticloadbalancing'
31
31
  spec.add_dependency 'aws-sdk-elasticloadbalancingv2'
32
32
  spec.add_dependency 'aws-sdk-s3'
33
+ spec.add_dependency 'aws-sdk-servicediscovery'
33
34
  spec.add_dependency 'aws-sdk-sns'
34
35
  spec.add_dependency 'aws-sdk-ssm'
35
36
  spec.add_dependency 'jsonnet'
@@ -27,6 +27,7 @@ module Hako
27
27
  memory_reservation
28
28
  links
29
29
  essential
30
+ entry_point
30
31
  command
31
32
  user
32
33
  privileged
@@ -14,6 +14,7 @@ require 'hako/schedulers/ecs_definition_comparator'
14
14
  require 'hako/schedulers/ecs_elb'
15
15
  require 'hako/schedulers/ecs_elb_v2'
16
16
  require 'hako/schedulers/ecs_service_comparator'
17
+ require 'hako/schedulers/ecs_service_discovery'
17
18
  require 'hako/schedulers/ecs_volume_comparator'
18
19
 
19
20
  module Hako
@@ -87,6 +88,9 @@ module Hako
87
88
  }
88
89
  end
89
90
  end
91
+ if options['service_discovery']
92
+ @service_discovery = EcsServiceDiscovery.new(options.fetch('service_discovery'), @region, dry_run: @dry_run)
93
+ end
90
94
 
91
95
  @started_at = nil
92
96
  @container_instance_arn = nil
@@ -114,6 +118,9 @@ module Hako
114
118
  @autoscaling.apply(Aws::ECS::Types::Service.new(cluster_arn: @cluster, service_name: @app_id))
115
119
  end
116
120
  ecs_elb_client.modify_attributes
121
+ if @service_discovery
122
+ @service_discovery.apply
123
+ end
117
124
  else
118
125
  current_service = describe_service
119
126
  task_definition_changed, task_definition = register_task_definition(definitions)
@@ -130,12 +137,18 @@ module Hako
130
137
  @autoscaling.apply(current_service)
131
138
  end
132
139
  ecs_elb_client.modify_attributes
140
+ if @service_discovery
141
+ @service_discovery.apply
142
+ end
133
143
  else
134
144
  Hako.logger.info "Updated service: #{service.service_arn}"
135
145
  if @autoscaling
136
146
  @autoscaling.apply(service)
137
147
  end
138
148
  ecs_elb_client.modify_attributes
149
+ if @service_discovery
150
+ @service_discovery.apply
151
+ end
139
152
  unless wait_for_ready(service)
140
153
  if task_definition_changed
141
154
  Hako.logger.error("Rolling back to #{current_service.task_definition}")
@@ -295,6 +308,13 @@ module Hako
295
308
  else
296
309
  puts 'Autoscaling: No'
297
310
  end
311
+
312
+ if service.service_registries.empty?
313
+ puts 'Service Discovery: No'
314
+ else
315
+ puts 'Service Discovery:'
316
+ @service_discovery.status(service.service_registries)
317
+ end
298
318
  end
299
319
 
300
320
  # @return [nil]
@@ -313,6 +333,9 @@ module Hako
313
333
  ecs_client.delete_service(cluster: service.cluster_arn, service: service.service_arn)
314
334
  Hako.logger.info "#{service.service_arn} is deleted"
315
335
  end
336
+ unless service.service_registries.empty?
337
+ @service_discovery.remove(service.service_registries)
338
+ end
316
339
  else
317
340
  puts "Service #{@app_id} doesn't exist"
318
341
  end
@@ -611,6 +634,7 @@ module Hako
611
634
  secrets: container.secrets,
612
635
  docker_labels: container.docker_labels,
613
636
  mount_points: container.mount_points,
637
+ entry_point: container.entry_point,
614
638
  command: container.command,
615
639
  privileged: container.privileged,
616
640
  linux_parameters: container.linux_parameters,
@@ -831,6 +855,7 @@ module Hako
831
855
  params[:desired_count] = current_service.desired_count
832
856
  end
833
857
  warn_placement_policy_change(current_service)
858
+ warn_service_registries_change(current_service)
834
859
  if service_changed?(current_service, params)
835
860
  ecs_client.update_service(params).service
836
861
  else
@@ -863,6 +888,10 @@ module Hako
863
888
  ecs_elb_client.modify_attributes
864
889
  params[:load_balancers] = [ecs_elb_client.load_balancer_params_for_service]
865
890
  end
891
+ if @service_discovery
892
+ @service_discovery.apply
893
+ params[:service_registries] = @service_discovery.service_registries
894
+ end
866
895
  ecs_client.create_service(params).service
867
896
  end
868
897
 
@@ -1203,6 +1232,9 @@ module Hako
1203
1232
  (definition[:docker_security_options] || []).each do |docker_security_option|
1204
1233
  cmd << '--security-opt' << docker_security_option
1205
1234
  end
1235
+ if definition[:entry_point]
1236
+ cmd << '--entrypoint' << definition[:entry_point]
1237
+ end
1206
1238
 
1207
1239
  cmd << "\\\n "
1208
1240
  definition.fetch(:environment).each do |env|
@@ -1283,6 +1315,16 @@ module Hako
1283
1315
  end
1284
1316
  end
1285
1317
 
1318
+ # @param [Aws::ECS::Types::Service] service
1319
+ # @return [void]
1320
+ def warn_service_registries_change(service)
1321
+ actual_service_registries = service.service_registries.sort_by(&:registry_arn).map(&:to_h)
1322
+ expected_service_registries = @service_discovery&.service_registries&.sort_by { |s| s[:registry_arn] } || []
1323
+ if actual_service_registries != expected_service_registries
1324
+ Hako.logger.warn "Ignoring updated service_registries in the configuration, because AWS doesn't allow updating them for now."
1325
+ end
1326
+ end
1327
+
1286
1328
  # @param [Aws::ECS::Types::TaskDefinition] task_definition
1287
1329
  # @param [String] target_definition
1288
1330
  # @return [nil]
@@ -32,6 +32,7 @@ module Hako
32
32
  struct.member(:secrets, Schema::Nullable.new(Schema::UnorderedArray.new(secrets_schema)))
33
33
  struct.member(:docker_labels, Schema::Table.new(Schema::String.new, Schema::String.new))
34
34
  struct.member(:mount_points, Schema::UnorderedArray.new(mount_point_schema))
35
+ struct.member(:entry_point, Schema::Nullable.new(Schema::OrderedArray.new(Schema::String.new)))
35
36
  struct.member(:command, Schema::Nullable.new(Schema::OrderedArray.new(Schema::String.new)))
36
37
  struct.member(:volumes_from, Schema::UnorderedArray.new(volumes_from_schema))
37
38
  struct.member(:user, Schema::Nullable.new(Schema::String.new))
@@ -0,0 +1,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-servicediscovery'
4
+ require 'hako'
5
+ require 'hako/error'
6
+ require 'hako/schedulers/ecs_service_discovery_service_comparator'
7
+
8
+ module Hako
9
+ module Schedulers
10
+ class EcsServiceDiscovery
11
+ # @param [Array<Hash>] config
12
+ # @param [Boolean] dry_run
13
+ # @param [String] region
14
+ def initialize(config, region, dry_run:)
15
+ @region = region
16
+ @config = config
17
+ @dry_run = dry_run
18
+ end
19
+
20
+ # @return [void]
21
+ def apply
22
+ @config.map do |service_discovery|
23
+ service = service_discovery.fetch('service')
24
+ namespace_id = service.fetch('namespace_id')
25
+ namespace = get_namespace(namespace_id)
26
+ if !namespace
27
+ raise Error.new("Service discovery namespace #{namespace_id} not found")
28
+ elsif namespace.type != 'DNS_PRIVATE'
29
+ raise Error.new("ECS only supports registering a service into a private DNS namespace: #{namespace.name} (#{namespace_id})")
30
+ end
31
+
32
+ service_name = service.fetch('name')
33
+ current_service = find_service(namespace_id, service_name)
34
+ if !current_service
35
+ if @dry_run
36
+ Hako.logger.info("Created service discovery service #{service_name} (dry-run)")
37
+ else
38
+ current_service = create_service(service)
39
+ Hako.logger.info("Created service discovery service #{service_name} (#{current_service.id})")
40
+ end
41
+ else
42
+ if service_changed?(service, current_service)
43
+ if @dry_run
44
+ Hako.logger.info("Updated service discovery service #{service_name} (#{current_service.id}) (dry-run)")
45
+ else
46
+ update_service(current_service.id, service)
47
+ Hako.logger.info("Updated service discovery service #{service_name} (#{current_service.id})")
48
+ end
49
+ end
50
+ warn_disallowed_service_change(service, current_service)
51
+ end
52
+ end
53
+ end
54
+
55
+ # @return [void]
56
+ def status(service_registries)
57
+ service_registries.each do |service_registry|
58
+ service_id = service_registry.registry_arn.slice(%r{service/(.+)\z}, 1)
59
+ service = get_service(service_id)
60
+ next unless service
61
+
62
+ namespace = get_namespace(service.namespace_id)
63
+ instances = service_discovery_client.list_instances(service_id: service.id).flat_map(&:instances)
64
+ puts " #{service.name}.#{namespace.name} instance_count=#{instances.size}"
65
+ instances.each do |instance|
66
+ instance_attributes = instance.attributes.map { |k, v| "#{k}=#{v}" }.join(', ')
67
+ puts " #{instance.id} #{instance_attributes}"
68
+ end
69
+ end
70
+ end
71
+
72
+ # @return [void]
73
+ def remove(service_registries)
74
+ service_registries.each do |service_registry|
75
+ service_id = service_registry.registry_arn.slice(%r{service/(.+)\z}, 1)
76
+ service = get_service(service_id)
77
+ unless service
78
+ Hako.logger.info("Service discovery service #{service_name} (#{service_id}) doesn't exist")
79
+ next
80
+ end
81
+ if @dry_run
82
+ Hako.logger.info("Deleted service discovery service #{service.name} (#{service.id}) (dry-run)")
83
+ else
84
+ deleted = false
85
+ 10.times do |i|
86
+ sleep 10 unless i.zero?
87
+ begin
88
+ service_discovery_client.delete_service(id: service.id)
89
+ deleted = true
90
+ break
91
+ rescue Aws::ServiceDiscovery::Errors::ResourceInUse => e
92
+ Hako.logger.warn("#{e.class}: #{e.message}")
93
+ end
94
+ end
95
+ unless deleted
96
+ raise Error.new("Unable to delete service discovery service #{service.name} (#{service.id})")
97
+ end
98
+
99
+ Hako.logger.info("Deleted service discovery service #{service.name} (#{service.id})")
100
+ end
101
+ end
102
+ end
103
+
104
+ # @return [Hash]
105
+ def service_registries
106
+ @config.map do |service_discovery|
107
+ service = service_discovery.fetch('service')
108
+ namespace_id = service.fetch('namespace_id')
109
+ service_name = service.fetch('name')
110
+ current_service = find_service(namespace_id, service_name)
111
+ unless current_service
112
+ raise Error.new("Service discovery service #{service_name} not found")
113
+ end
114
+
115
+ {
116
+ container_name: service_discovery['container_name'],
117
+ container_port: service_discovery['container_port'],
118
+ port: service_discovery['port'],
119
+ registry_arn: current_service.arn,
120
+ }.reject { |_, v| v.nil? }
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ # @param [String] namespace_id
127
+ # @param [String] service_name
128
+ # @return [Aws::ServiceDiscovery::Types::ServiceSummary, nil]
129
+ def find_service(namespace_id, service_name)
130
+ params = {
131
+ filters: [
132
+ name: 'NAMESPACE_ID',
133
+ values: [namespace_id],
134
+ condition: 'EQ',
135
+ ],
136
+ }
137
+ services = service_discovery_client.list_services(params).flat_map(&:services)
138
+ services.find { |service| service.name == service_name }
139
+ end
140
+
141
+ # @return [Aws::ServiceDiscovery::Client]
142
+ def service_discovery_client
143
+ @service_discovery_client ||= Aws::ServiceDiscovery::Client.new(region: @region)
144
+ end
145
+
146
+ # @param [Hash] service
147
+ # @return [Aws::ServiceDiscovery::Types::Service]
148
+ def create_service(service)
149
+ service_discovery_client.create_service(create_service_params(service)).service
150
+ end
151
+
152
+ # @param [Hash] service
153
+ # @return [Hash]
154
+ def create_service_params(service)
155
+ dns_config = service.fetch('dns_config')
156
+ params = {
157
+ name: service.fetch('name'),
158
+ namespace_id: service['namespace_id'],
159
+ description: service['description'],
160
+ dns_config: {
161
+ namespace_id: dns_config['namespace_id'],
162
+ routing_policy: dns_config.fetch('routing_policy', 'MULTIVALUE'),
163
+ },
164
+ }
165
+ params[:dns_config][:dns_records] = dns_config.fetch('dns_records').map do |dns_record|
166
+ {
167
+ type: dns_record.fetch('type'),
168
+ ttl: dns_record.fetch('ttl'),
169
+ }
170
+ end
171
+ if (health_check_custom_config = service['health_check_custom_config'])
172
+ params[:health_check_custom_config] = {
173
+ failure_threshold: health_check_custom_config['failure_threshold'],
174
+ }
175
+ end
176
+ params
177
+ end
178
+
179
+ # @param [Hash] expected_service
180
+ # @param [Aws::ServiceDiscovery::Types::ServiceSummary] actual_service
181
+ # @return [Boolean]
182
+ def service_changed?(expected_service, actual_service)
183
+ EcsServiceDiscoveryServiceComparator.new(update_service_params(expected_service)).different?(actual_service)
184
+ end
185
+
186
+ # @param [String] service_id
187
+ # @param [Hash] service
188
+ def update_service(service_id, service)
189
+ operation_id = service_discovery_client.update_service(
190
+ id: service_id,
191
+ service: update_service_params(service),
192
+ ).operation_id
193
+ operation = wait_for_operation(operation_id)
194
+ if operation.status != 'SUCCESS'
195
+ raise Error.new("Unable to update service discovery service (#{operation.error_code}): #{operation.error_message}")
196
+ end
197
+ end
198
+
199
+ # @param [Hash] service
200
+ # @return [Hash]
201
+ def update_service_params(service)
202
+ dns_config = service.fetch('dns_config')
203
+ params = {
204
+ description: service['description'],
205
+ dns_config: {},
206
+ }
207
+ params[:dns_config][:dns_records] = dns_config.fetch('dns_records').map do |dns_record|
208
+ {
209
+ type: dns_record.fetch('type'),
210
+ ttl: dns_record.fetch('ttl'),
211
+ }
212
+ end
213
+ params
214
+ end
215
+
216
+ # @param [String] service_id
217
+ # @return [Aws::ServiceDiscovery::Types::GetOperationResponse]
218
+ def wait_for_operation(operation_id)
219
+ loop do
220
+ operation = service_discovery_client.get_operation(operation_id: operation_id).operation
221
+ return operation if %w[SUCCESS FAIL].include?(operation.status)
222
+
223
+ sleep 10
224
+ end
225
+ end
226
+
227
+ # @param [String] service_id
228
+ # @return [Aws::ServiceDiscovery::Types::Service, nil]
229
+ def get_service(service_id)
230
+ service_discovery_client.get_service(id: service_id).service
231
+ rescue Aws::ServiceDiscovery::Errors::ServiceNotFound
232
+ nil
233
+ end
234
+
235
+ # @param [String] namespace_id
236
+ # @return [Aws::ServiceDiscovery::Types::Namespace, nil]
237
+ def get_namespace(namespace_id)
238
+ service_discovery_client.get_namespace(id: namespace_id).namespace
239
+ rescue Aws::ServiceDiscovery::Errors::NamespaceNotFound
240
+ nil
241
+ end
242
+
243
+ # @param [Hash] expected_service
244
+ # @param [Aws::ServiceDiscovery::Types::ServiceSummary] actual_service
245
+ # @return [void]
246
+ def warn_disallowed_service_change(expected_service, actual_service)
247
+ expected_service = create_service_params(expected_service)
248
+ if expected_service.dig(:dns_config, :routing_policy) != actual_service.dns_config.routing_policy
249
+ Hako.logger.warn("Ignoring updated service_discovery.dns_config.routing_policy in the configuration, because AWS doesn't allow updating it for now.")
250
+ end
251
+ if expected_service[:health_check_custom_config] != actual_service.health_check_custom_config&.to_h
252
+ Hako.logger.warn("Ignoring updated service_discovery.health_check_custom_config in the configuration, because AWS doesn't allow updating it for now.")
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hako/schema'
4
+
5
+ module Hako
6
+ module Schedulers
7
+ class EcsServiceDiscoveryServiceComparator
8
+ # @param [Hash] expected_service
9
+ def initialize(expected_service)
10
+ @expected_service = expected_service
11
+ @schema = service_schema
12
+ end
13
+
14
+ # @param [Aws::ServiceDiscovery::Types::ServiceSummary] actual_service
15
+ # @return [Boolean]
16
+ def different?(actual_service)
17
+ !@schema.same?(actual_service.to_h, @expected_service)
18
+ end
19
+
20
+ private
21
+
22
+ def service_schema
23
+ Schema::Structure.new.tap do |struct|
24
+ struct.member(:description, Schema::Nullable.new(Schema::String.new))
25
+ struct.member(:dns_config, dns_config_schema)
26
+ end
27
+ end
28
+
29
+ def dns_config_schema
30
+ Schema::Structure.new.tap do |struct|
31
+ struct.member(:dns_records, Schema::UnorderedArray.new(dns_records_schema))
32
+ end
33
+ end
34
+
35
+ def dns_records_schema
36
+ Schema::Structure.new.tap do |struct|
37
+ struct.member(:ttl, Schema::Integer.new)
38
+ struct.member(:type, Schema::String.new)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hako
4
- VERSION = '2.6.2'
4
+ VERSION = '2.7.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.6.2
4
+ version: 2.7.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-12-19 00:00:00.000000000 Z
11
+ date: 2019-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-applicationautoscaling
@@ -136,6 +136,20 @@ dependencies:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: aws-sdk-servicediscovery
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
139
153
  - !ruby/object:Gem::Dependency
140
154
  name: aws-sdk-sns
141
155
  requirement: !ruby/object:Gem::Requirement
@@ -312,6 +326,7 @@ files:
312
326
  - examples/hello-lb.jsonnet
313
327
  - examples/hello-nofront.jsonnet
314
328
  - examples/hello-privileged-app.jsonnet
329
+ - examples/hello-service-discovery.jsonnet
315
330
  - examples/hello.env
316
331
  - examples/hello.jsonnet
317
332
  - examples/put-ecs-container-status-to-s3/index.js
@@ -341,6 +356,8 @@ files:
341
356
  - lib/hako/schedulers/ecs_elb.rb
342
357
  - lib/hako/schedulers/ecs_elb_v2.rb
343
358
  - lib/hako/schedulers/ecs_service_comparator.rb
359
+ - lib/hako/schedulers/ecs_service_discovery.rb
360
+ - lib/hako/schedulers/ecs_service_discovery_service_comparator.rb
344
361
  - lib/hako/schedulers/ecs_volume_comparator.rb
345
362
  - lib/hako/schema.rb
346
363
  - lib/hako/schema/boolean.rb
@@ -380,7 +397,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
380
397
  version: '0'
381
398
  requirements: []
382
399
  rubyforge_project:
383
- rubygems_version: 2.7.6
400
+ rubygems_version: 2.7.6.2
384
401
  signing_key:
385
402
  specification_version: 4
386
403
  summary: Deploy Docker container