cloud-mu 2.0.4 → 2.1.0beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +6 -0
  3. data/ansible/roles/geerlingguy.firewall/LICENSE +20 -0
  4. data/ansible/roles/geerlingguy.firewall/README.md +93 -0
  5. data/ansible/roles/geerlingguy.firewall/defaults/main.yml +19 -0
  6. data/ansible/roles/geerlingguy.firewall/handlers/main.yml +3 -0
  7. data/ansible/roles/geerlingguy.firewall/meta/main.yml +26 -0
  8. data/ansible/roles/geerlingguy.firewall/molecule/default/molecule.yml +40 -0
  9. data/ansible/roles/geerlingguy.firewall/molecule/default/playbook.yml +17 -0
  10. data/ansible/roles/geerlingguy.firewall/molecule/default/tests/test_default.py +14 -0
  11. data/ansible/roles/geerlingguy.firewall/molecule/default/yaml-lint.yml +6 -0
  12. data/ansible/roles/geerlingguy.firewall/tasks/disable-other-firewalls.yml +66 -0
  13. data/ansible/roles/geerlingguy.firewall/tasks/main.yml +44 -0
  14. data/ansible/roles/geerlingguy.firewall/templates/firewall.bash.j2 +136 -0
  15. data/ansible/roles/geerlingguy.firewall/templates/firewall.init.j2 +52 -0
  16. data/ansible/roles/geerlingguy.firewall/templates/firewall.unit.j2 +12 -0
  17. data/bin/mu-ansible-secret +114 -0
  18. data/bin/mu-aws-setup +74 -21
  19. data/bin/mu-node-manage +22 -12
  20. data/bin/mu-self-update +11 -4
  21. data/cloud-mu.gemspec +3 -3
  22. data/cookbooks/firewall/metadata.json +1 -1
  23. data/cookbooks/firewall/recipes/default.rb +4 -0
  24. data/cookbooks/mu-master/recipes/default.rb +0 -3
  25. data/cookbooks/mu-master/recipes/init.rb +15 -9
  26. data/cookbooks/mu-master/templates/default/mu.rc.erb +1 -1
  27. data/cookbooks/mu-master/templates/default/web_app.conf.erb +0 -4
  28. data/cookbooks/mu-php54/metadata.rb +2 -2
  29. data/cookbooks/mu-php54/recipes/default.rb +1 -3
  30. data/cookbooks/mu-tools/recipes/eks.rb +25 -2
  31. data/cookbooks/mu-tools/recipes/nrpe.rb +6 -1
  32. data/cookbooks/mu-tools/recipes/set_mu_hostname.rb +8 -0
  33. data/cookbooks/mu-tools/templates/default/etc_hosts.erb +1 -1
  34. data/cookbooks/mu-tools/templates/default/kubeconfig.erb +2 -2
  35. data/cookbooks/mu-tools/templates/default/kubelet-config.json.erb +35 -0
  36. data/extras/clean-stock-amis +10 -4
  37. data/extras/list-stock-amis +64 -0
  38. data/extras/python_rpm/build.sh +21 -0
  39. data/extras/python_rpm/muthon.spec +68 -0
  40. data/install/README.md +5 -2
  41. data/install/user-dot-murc.erb +1 -1
  42. data/modules/mu.rb +52 -8
  43. data/modules/mu/clouds/aws.rb +1 -1
  44. data/modules/mu/clouds/aws/container_cluster.rb +1071 -47
  45. data/modules/mu/clouds/aws/firewall_rule.rb +45 -19
  46. data/modules/mu/clouds/aws/log.rb +3 -2
  47. data/modules/mu/clouds/aws/role.rb +18 -2
  48. data/modules/mu/clouds/aws/server.rb +11 -5
  49. data/modules/mu/clouds/aws/server_pool.rb +20 -24
  50. data/modules/mu/clouds/aws/userdata/linux.erb +1 -1
  51. data/modules/mu/clouds/aws/vpc.rb +9 -0
  52. data/modules/mu/clouds/google/server.rb +2 -0
  53. data/modules/mu/config.rb +3 -3
  54. data/modules/mu/config/container_cluster.rb +1 -1
  55. data/modules/mu/config/firewall_rule.rb +4 -0
  56. data/modules/mu/config/role.rb +29 -0
  57. data/modules/mu/config/server.rb +9 -4
  58. data/modules/mu/groomer.rb +14 -3
  59. data/modules/mu/groomers/ansible.rb +553 -0
  60. data/modules/mu/groomers/chef.rb +0 -5
  61. data/modules/mu/mommacat.rb +18 -3
  62. data/modules/scratchpad.erb +1 -1
  63. data/requirements.txt +5 -0
  64. metadata +39 -16
data/install/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Cloudamatic Mu Master Installation
2
- Create a VPC and manually provision a Mu Master. Creation gives you full control over the shape of the master VPC and individual settings
2
+ There are two paths to creating a Mu Master.
3
3
 
4
- For detailed instructions on installation techniques see [our Wiki Installation page](https://github.com/cloudamatic/mu/wiki/Install-Home)
4
+ - **Typical Installation**: The simplest and recommended path is to use our CloudFormation script to configure an appropriate Virtual Private Cloud and master with all features enabled, including both a command line and Jenkins GUI user interface.
5
+ - **Custom Installation:** If you prefer, you can also create your own VPC and manually provision a Mu Master. This gives you more control over the shape of the master VPC and individual settings
6
+
7
+ For detailed instructions on both installation techniques see [our Wiki Installation page](https://github.com/cloudamatic/mu/wiki/Install-Home)
5
8
  For mu master usage instructions see [our Wiki usage page](https://github.com/cloudamatic/mu/wiki/Usage)
@@ -1,6 +1,6 @@
1
1
  export MU_DATADIR="<%= home %>/.mu/var"
2
2
  export MU_CHEF_CACHE="<%= home %>/.chef"
3
- export PATH="<%= installdir %>/bin:/usr/local/ruby-current/bin:${PATH}:/opt/opscode/embedded/bin"
3
+ export PATH="<%= installdir %>/bin:/usr/local/ruby-current/bin:/usr/local/python-current/bin:${PATH}:/opt/opscode/embedded/bin"
4
4
 
5
5
  if [ ! -f "<%= home %>/.first_chef_upload" -a "`tty`" != "not a tty" ];then
6
6
  touch "<%= home %>/.first_chef_upload"
data/modules/mu.rb CHANGED
@@ -214,11 +214,12 @@ module MU
214
214
  @myDataDir = File.expand_path(ENV['MU_DATADIR']) if ENV.has_key?("MU_DATADIR")
215
215
  @myDataDir = @@mainDataDir if @myDataDir.nil?
216
216
  # Mu's deployment metadata directory.
217
- def self.dataDir
218
- if MU.mu_user.nil? or MU.mu_user.empty? or MU.mu_user == "mu" or MU.mu_user == "root"
217
+ def self.dataDir(for_user = MU.mu_user)
218
+ if for_user.nil? or for_user.empty? or for_user == "mu" or for_user == "root"
219
219
  return @myDataDir
220
220
  else
221
- basepath = Etc.getpwnam(MU.mu_user).dir+"/.mu"
221
+ for_user ||= MU.mu_user
222
+ basepath = Etc.getpwnam(for_user).dir+"/.mu"
222
223
  Dir.mkdir(basepath, 0755) if !Dir.exists?(basepath)
223
224
  Dir.mkdir(basepath+"/var", 0755) if !Dir.exists?(basepath+"/var")
224
225
  return basepath+"/var"
@@ -426,7 +427,7 @@ module MU
426
427
  # XXX these guys to move into mu/groomer
427
428
  # List of known/supported grooming agents (configuration management tools)
428
429
  def self.supportedGroomers
429
- ["Chef"]
430
+ ["Chef", "Ansible"]
430
431
  end
431
432
 
432
433
  MU.supportedGroomers.each { |groomer|
@@ -626,10 +627,38 @@ module MU
626
627
  true
627
628
  end
628
629
 
630
+ # Given a hash, or an array that might contain a hash, change all of the keys
631
+ # to symbols. Useful for formatting option parameters to some APIs.
632
+ def self.strToSym(obj)
633
+ if obj.is_a?(Hash)
634
+ newhash = {}
635
+ obj.each_pair { |k, v|
636
+ if v.is_a?(Hash) or v.is_a?(Array)
637
+ newhash[k.to_sym] = MU.strToSym(v)
638
+ else
639
+ newhash[k.to_sym] = v
640
+ end
641
+ }
642
+ newhash
643
+ elsif obj.is_a?(Array)
644
+ newarr = []
645
+ obj.each { |v|
646
+ if v.is_a?(Hash) or v.is_a?(Array)
647
+ newarr << MU.strToSym(v)
648
+ else
649
+ newarr << v
650
+ end
651
+ }
652
+ newarr
653
+ end
654
+ end
655
+
656
+
629
657
  # Recursively turn a Ruby OpenStruct into a Hash
630
658
  # @param struct [OpenStruct]
659
+ # @param stringify_keys [Boolean]
631
660
  # @return [Hash]
632
- def self.structToHash(struct)
661
+ def self.structToHash(struct, stringify_keys: false)
633
662
  google_struct = false
634
663
  begin
635
664
  google_struct = struct.class.ancestors.include?(::Google::Apis::Core::Hashable)
@@ -646,18 +675,33 @@ module MU
646
675
  google_struct or aws_struct
647
676
 
648
677
  hash = struct.to_h
678
+ if stringify_keys
679
+ newhash = {}
680
+ hash.each_pair { |k, v|
681
+ newhash[k.to_s] = v
682
+ }
683
+ hash = newhash
684
+ end
685
+
649
686
  hash.each_pair { |key, value|
650
- hash[key] = self.structToHash(value)
687
+ hash[key] = self.structToHash(value, stringify_keys: stringify_keys)
651
688
  }
652
689
  return hash
653
690
  elsif struct.is_a?(Hash)
691
+ if stringify_keys
692
+ newhash = {}
693
+ struct.each_pair { |k, v|
694
+ newhash[k.to_s] = v
695
+ }
696
+ struct = newhash
697
+ end
654
698
  struct.each_pair { |key, value|
655
- struct[key] = self.structToHash(value)
699
+ struct[key] = self.structToHash(value, stringify_keys: stringify_keys)
656
700
  }
657
701
  return struct
658
702
  elsif struct.is_a?(Array)
659
703
  struct.map! { |elt|
660
- self.structToHash(elt)
704
+ self.structToHash(elt, stringify_keys: stringify_keys)
661
705
  }
662
706
  else
663
707
  return struct
@@ -1239,7 +1239,7 @@ module MU
1239
1239
  retval = @api.method(method_sym).call
1240
1240
  end
1241
1241
  return retval
1242
- rescue Aws::EC2::Errors::InternalError, Aws::EC2::Errors::RequestLimitExceeded, Aws::EC2::Errors::Unavailable, Aws::Route53::Errors::Throttling, Aws::ElasticLoadBalancing::Errors::HttpFailureException, Aws::EC2::Errors::Http503Error, Aws::AutoScaling::Errors::Http503Error, Aws::AutoScaling::Errors::InternalFailure, Aws::AutoScaling::Errors::ServiceUnavailable, Aws::Route53::Errors::ServiceUnavailable, Aws::ElasticLoadBalancing::Errors::Throttling, Aws::RDS::Errors::ClientUnavailable, Aws::Waiters::Errors::UnexpectedError, Aws::ElasticLoadBalancing::Errors::ServiceUnavailable, Aws::ElasticLoadBalancingV2::Errors::Throttling, Seahorse::Client::NetworkingError, Aws::IAM::Errors::Throttling, Aws::EFS::Errors::ThrottlingException, Aws::Pricing::Errors::ThrottlingException, Aws::APIGateway::Errors::TooManyRequestsException => e
1242
+ rescue Aws::EC2::Errors::InternalError, Aws::EC2::Errors::RequestLimitExceeded, Aws::EC2::Errors::Unavailable, Aws::Route53::Errors::Throttling, Aws::ElasticLoadBalancing::Errors::HttpFailureException, Aws::EC2::Errors::Http503Error, Aws::AutoScaling::Errors::Http503Error, Aws::AutoScaling::Errors::InternalFailure, Aws::AutoScaling::Errors::ServiceUnavailable, Aws::Route53::Errors::ServiceUnavailable, Aws::ElasticLoadBalancing::Errors::Throttling, Aws::RDS::Errors::ClientUnavailable, Aws::Waiters::Errors::UnexpectedError, Aws::ElasticLoadBalancing::Errors::ServiceUnavailable, Aws::ElasticLoadBalancingV2::Errors::Throttling, Seahorse::Client::NetworkingError, Aws::IAM::Errors::Throttling, Aws::EFS::Errors::ThrottlingException, Aws::Pricing::Errors::ThrottlingException, Aws::APIGateway::Errors::TooManyRequestsException, Aws::ECS::Errors::ThrottlingException => e
1243
1243
  if e.class.name == "Seahorse::Client::NetworkingError" and e.message.match(/Name or service not known/)
1244
1244
  MU.log e.inspect, MU::ERR
1245
1245
  raise e
@@ -61,16 +61,28 @@ module MU
61
61
 
62
62
  resp = nil
63
63
  begin
64
- MU.log "Creating EKS cluster #{@mu_name}"
65
- resp = MU::Cloud::AWS.eks(region: @config['region'], credentials: @config['credentials']).create_cluster(
66
- name: @mu_name,
67
- version: @config['kubernetes']['version'],
68
- role_arn: role_arn,
69
- resources_vpc_config: {
70
- security_group_ids: security_groups,
71
- subnet_ids: subnet_ids
64
+ params = {
65
+ :name => @mu_name,
66
+ :version => @config['kubernetes']['version'],
67
+ :role_arn => role_arn,
68
+ :resources_vpc_config => {
69
+ :security_group_ids => security_groups,
70
+ :subnet_ids => subnet_ids
72
71
  }
73
- )
72
+ }
73
+ if @config['logging'] and @config['logging'].size > 0
74
+ params[:logging] = {
75
+ :cluster_logging => [
76
+ {
77
+ :types => @config['logging'],
78
+ :enabled => true
79
+ }
80
+ ]
81
+ }
82
+ end
83
+
84
+ MU.log "Creating EKS cluster #{@mu_name}", details: params
85
+ resp = MU::Cloud::AWS.eks(region: @config['region'], credentials: @config['credentials']).create_cluster(params)
74
86
  rescue Aws::EKS::Errors::UnsupportedAvailabilityZoneException => e
75
87
  # this isn't the dumbest thing we've ever done, but it's up there
76
88
  if e.message.match(/because (#{Regexp.quote(@config['region'])}[a-z]), the targeted availability zone, does not currently have sufficient capacity/)
@@ -130,6 +142,7 @@ module MU
130
142
  MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).create_cluster(
131
143
  cluster_name: @mu_name
132
144
  )
145
+
133
146
  end
134
147
  @cloud_id = @mu_name
135
148
  end
@@ -181,14 +194,14 @@ module MU
181
194
  }
182
195
 
183
196
  authmap_cmd = %Q{/opt/mu/bin/kubectl --kubeconfig "#{kube_conf}" apply -f "#{eks_auth}"}
184
- MU.log "Configuring Kubernetes <=> IAM mapping for worker nodes", details: authmap_cmd
197
+ MU.log "Configuring Kubernetes <=> IAM mapping for worker nodes", MU::NOTICE, details: authmap_cmd
185
198
  # maybe guard this mess
186
199
  %x{#{authmap_cmd}}
187
200
 
188
201
  # and this one
189
202
  admin_user_cmd = %Q{/opt/mu/bin/kubectl --kubeconfig "#{kube_conf}" apply -f "#{MU.myRoot}/extras/admin-user.yaml"}
190
203
  admin_role_cmd = %Q{/opt/mu/bin/kubectl --kubeconfig "#{kube_conf}" apply -f "#{MU.myRoot}/extras/admin-role-binding.yaml"}
191
- MU.log "Configuring Kubernetes admin-user and role", details: admin_user_cmd+"\n"+admin_role_cmd
204
+ MU.log "Configuring Kubernetes admin-user and role", MU::NOTICE, details: admin_user_cmd+"\n"+admin_role_cmd
192
205
  %x{#{admin_user_cmd}}
193
206
  %x{#{admin_role_cmd}}
194
207
 
@@ -213,8 +226,8 @@ module MU
213
226
  }
214
227
  end
215
228
 
216
- MU.log %Q{How to interact with your Kubernetes cluster\nkubectl --kubeconfig "#{kube_conf}" get all\nkubectl --kubeconfig "#{kube_conf}" create -f some_k8s_deploy.yml}, MU::SUMMARY
217
- else
229
+ MU.log %Q{How to interact with your Kubernetes cluster\nkubectl --kubeconfig "#{kube_conf}" get all\nkubectl --kubeconfig "#{kube_conf}" create -f some_k8s_deploy.yml\nkubectl --kubeconfig "#{kube_conf}" get nodes}, MU::SUMMARY
230
+ elsif @config['flavor'] != "Fargate"
218
231
  resp = MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).list_container_instances({
219
232
  cluster: @mu_name
220
233
  })
@@ -281,7 +294,323 @@ module MU
281
294
  }
282
295
  }
283
296
  end
284
- # launch_type: "EC2" only option in GovCloud
297
+
298
+ if @config['flavor'] != "EKS" and @config['containers']
299
+
300
+ security_groups = []
301
+ if @dependencies.has_key?("firewall_rule")
302
+ @dependencies['firewall_rule'].values.each { |sg|
303
+ security_groups << sg.cloud_id
304
+ }
305
+ end
306
+
307
+ tasks_registered = 0
308
+ retries = 0
309
+ svc_resp = begin
310
+ MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).list_services(
311
+ cluster: arn
312
+ )
313
+ rescue Aws::ECS::Errors::ClusterNotFoundException => e
314
+ if retries < 10
315
+ sleep 5
316
+ retries += 1
317
+ retry
318
+ else
319
+ raise e
320
+ end
321
+ end
322
+ existing_svcs = svc_resp.service_arns.map { |s|
323
+ s.gsub(/.*?:service\/(.*)/, '\1')
324
+ }
325
+
326
+ # Reorganize things so that we have services and task definitions
327
+ # mapped to the set of containers they must contain
328
+ tasks = {}
329
+ created_generic_loggroup = false
330
+
331
+ @config['containers'].each { |c|
332
+ service_name = c['service'] ? @mu_name+"-"+c['service'].upcase : @mu_name+"-"+c['name'].upcase
333
+ tasks[service_name] ||= []
334
+ tasks[service_name] << c
335
+ }
336
+
337
+ tasks.each_pair { |service_name, containers|
338
+ launch_type = @config['flavor'] == "ECS" ? "EC2" : "FARGATE"
339
+ cpu_total = 0
340
+ mem_total = 0
341
+ role_arn = nil
342
+
343
+ container_definitions = containers.map { |c|
344
+ cpu_total += c['cpu']
345
+ mem_total += c['memory']
346
+
347
+ if c["role"] and !role_arn
348
+ found = MU::MommaCat.findStray(
349
+ @config['cloud'],
350
+ "role",
351
+ cloud_id: c["role"]["id"],
352
+ name: c["role"]["name"],
353
+ deploy_id: c["role"]["deploy_id"] || @deploy.deploy_id,
354
+ dummy_ok: false
355
+ )
356
+ if found
357
+ found = found.first
358
+ if found and found.cloudobj
359
+ role_arn = found.cloudobj.arn
360
+ end
361
+ else
362
+ raise MuError, "Unable to find execution role from #{c["role"]}"
363
+ end
364
+ end
365
+
366
+ params = {
367
+ name: @mu_name+"-"+c['name'].upcase,
368
+ image: c['image'],
369
+ memory: c['memory'],
370
+ cpu: c['cpu']
371
+ }
372
+ if !@config['vpc']
373
+ c['hostname'] ||= @mu_name+"-"+c['name'].upcase
374
+ end
375
+ [:essential, :hostname, :start_timeout, :stop_timeout, :user, :working_directory, :disable_networking, :privileged, :readonly_root_filesystem, :interactive, :pseudo_terminal, :links, :entry_point, :command, :dns_servers, :dns_search_domains, :docker_security_options, :port_mappings, :repository_credentials, :mount_points, :environment, :volumes_from, :secrets, :depends_on, :extra_hosts, :docker_labels, :ulimits, :system_controls, :health_check, :resource_requirements].each { |param|
376
+ if c.has_key?(param.to_s)
377
+ params[param] = if !c[param.to_s].nil? and (c[param.to_s].is_a?(Hash) or c[param.to_s].is_a?(Array))
378
+ MU.strToSym(c[param.to_s])
379
+ else
380
+ c[param.to_s]
381
+ end
382
+ end
383
+ }
384
+ if @config['vpc']
385
+ [:hostname, :dns_servers, :dns_search_domains, :links].each { |param|
386
+ if params[param]
387
+ MU.log "Container parameter #{param.to_s} not supported in VPC clusters, ignoring", MU::WARN
388
+ params.delete(param)
389
+ end
390
+ }
391
+ end
392
+ if @config['flavor'] == "Fargate"
393
+ [:privileged, :docker_security_options].each { |param|
394
+ if params[param]
395
+ MU.log "Container parameter #{param.to_s} not supported in Fargate clusters, ignoring", MU::WARN
396
+ params.delete(param)
397
+ end
398
+ }
399
+ end
400
+ if c['log_configuration']
401
+ log_obj = @deploy.findLitterMate(name: c['log_configuration']['options']['awslogs-group'], type: "logs")
402
+ if log_obj
403
+ c['log_configuration']['options']['awslogs-group'] = log_obj.mu_name
404
+ end
405
+ params[:log_configuration] = MU.strToSym(c['log_configuration'])
406
+ end
407
+ params
408
+ }
409
+
410
+ cpu_total = 2 if cpu_total == 0
411
+ mem_total = 2 if mem_total == 0
412
+
413
+ task_params = {
414
+ family: @deploy.deploy_id,
415
+ container_definitions: container_definitions,
416
+ requires_compatibilities: [launch_type]
417
+ }
418
+
419
+ if @config['volumes']
420
+ task_params[:volumes] = []
421
+ @config['volumes'].each { |v|
422
+ vol = { :name => v['name'] }
423
+ if v['type'] == "host"
424
+ vol[:host] = {}
425
+ if v['host_volume_source_path']
426
+ vol[:host][:source_path] = v['host_volume_source_path']
427
+ end
428
+ elsif v['type'] == "docker"
429
+ vol[:docker_volume_configuration] = MU.strToSym(v['docker_volume_configuration'])
430
+ else
431
+ raise MuError, "Invalid volume type '#{v['type']}' specified in ContainerCluster '#{@mu_name}'"
432
+ end
433
+ task_params[:volumes] << vol
434
+ }
435
+ end
436
+
437
+ if role_arn
438
+ task_params[:execution_role_arn] = role_arn
439
+ task_params[:task_role_arn] = role_arn
440
+ end
441
+ if @config['flavor'] == "Fargate"
442
+ task_params[:network_mode] = "awsvpc"
443
+ task_params[:cpu] = cpu_total.to_i.to_s
444
+ task_params[:memory] = mem_total.to_i.to_s
445
+ end
446
+
447
+ tasks_registered += 1
448
+ MU.log "Registering task definition #{service_name} with #{container_definitions.size.to_s} containers"
449
+
450
+ # XXX this helpfully keeps revisions, but let's compare anyway and avoid cluttering with identical ones
451
+ resp = MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).register_task_definition(task_params)
452
+
453
+ task_def = resp.task_definition.task_definition_arn
454
+
455
+ service_params = {
456
+ :cluster => @mu_name,
457
+ :desired_count => @config['instance_count'], # XXX this makes no sense
458
+ :service_name => service_name,
459
+ :launch_type => launch_type,
460
+ :task_definition => task_def
461
+ }
462
+ if @config['vpc']
463
+ subnet_ids = []
464
+ all_public = true
465
+ subnet_names = @config['vpc']['subnets'].map { |s| s.values.first }
466
+ @vpc.subnets.each { |subnet_obj|
467
+ next if !subnet_names.include?(subnet_obj.config['name'])
468
+ subnet_ids << subnet_obj.cloud_id
469
+ all_public = false if subnet_obj.private?
470
+ }
471
+ service_params[:network_configuration] = {
472
+ :awsvpc_configuration => {
473
+ :subnets => subnet_ids,
474
+ :security_groups => security_groups,
475
+ :assign_public_ip => all_public ? "ENABLED" : "DISABLED"
476
+ }
477
+ }
478
+ end
479
+
480
+ if !existing_svcs.include?(service_name)
481
+ MU.log "Creating Service #{service_name}"
482
+
483
+ resp = MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).create_service(service_params)
484
+ else
485
+ service_params[:service] = service_params[:service_name].dup
486
+ service_params.delete(:service_name)
487
+ service_params.delete(:launch_type)
488
+ MU.log "Updating Service #{service_name}", MU::NOTICE, details: service_params
489
+
490
+ resp = MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).update_service(service_params)
491
+ end
492
+ existing_svcs << service_name
493
+ }
494
+
495
+ max_retries = 10
496
+ retries = 0
497
+ if tasks_registered > 0
498
+ retry_me = false
499
+ begin
500
+ retry_me = !MU::Cloud::AWS::ContainerCluster.tasksRunning?(@mu_name, log: (retries > 0), region: @config['region'], credentials: @config['credentials'])
501
+ retries += 1
502
+ sleep 15 if retry_me
503
+ end while retry_me and retries < max_retries
504
+ tasks = nil
505
+
506
+ if retry_me
507
+ MU.log "Not all tasks successfully launched in cluster #{@mu_name}", MU::WARN
508
+ end
509
+ end
510
+
511
+ end
512
+
513
+ end
514
+
515
+ # Returns true if all tasks in the given ECS/Fargate cluster are in the
516
+ # RUNNING state.
517
+ # @param cluster [String]: The cluster to check
518
+ # @param log [Boolean]: Output the state of each task to Mu's logger facility
519
+ # @param region [String]
520
+ # @param credentials [String]
521
+ # @return [Boolean]
522
+ def self.tasksRunning?(cluster, log: true, region: MU.myRegion, credentials: nil)
523
+ services = MU::Cloud::AWS.ecs(region: region, credentials: credentials).list_services(
524
+ cluster: cluster
525
+ ).service_arns.map { |s| s.sub(/.*?:service\/([^\/:]+?)$/, '\1') }
526
+
527
+ tasks_defined = []
528
+
529
+ begin
530
+ listme = services.slice!(0, (services.length >= 10 ? 10 : services.length))
531
+ if services.size > 0
532
+ tasks_defined.concat(
533
+ tasks = MU::Cloud::AWS.ecs(region: region, credentials: credentials).describe_services(
534
+ cluster: cluster,
535
+ services: listme
536
+ ).services.map { |s| s.task_definition }
537
+ )
538
+ end
539
+ end while services.size > 0
540
+
541
+ containers = {}
542
+
543
+ tasks_defined.each { |t|
544
+ taskdef = MU::Cloud::AWS.ecs(region: region, credentials: credentials).describe_task_definition(
545
+ task_definition: t.sub(/^.*?:task-definition\/([^\/:]+)$/, '\1')
546
+ )
547
+ taskdef.task_definition.container_definitions.each { |c|
548
+ containers[c.name] = {}
549
+ }
550
+ }
551
+
552
+ tasks = MU::Cloud::AWS.ecs(region: region, credentials: credentials).list_tasks(
553
+ cluster: cluster,
554
+ desired_status: "RUNNING"
555
+ ).task_arns
556
+
557
+ tasks.concat(MU::Cloud::AWS.ecs(region: region, credentials: credentials).list_tasks(
558
+ cluster: cluster,
559
+ desired_status: "STOPPED"
560
+ ).task_arns)
561
+
562
+ begin
563
+ sample = tasks.slice!(0, (tasks.length >= 100 ? 100 : tasks.length))
564
+ break if sample.size == 0
565
+ task_ids = sample.map { |task_arn|
566
+ task_arn.sub(/^.*?:task\/([a-f0-9\-]+)$/, '\1')
567
+ }
568
+
569
+ MU::Cloud::AWS.ecs(region: region, credentials: credentials).describe_tasks(
570
+ cluster: cluster,
571
+ tasks: task_ids
572
+ ).tasks.each { |t|
573
+ task_name = t.task_definition_arn.sub(/^.*?:task-definition\/([^\/:]+)$/, '\1')
574
+ t.containers.each { |c|
575
+ containers[c.name] ||= {}
576
+ containers[c.name][t.desired_status] ||= {
577
+ "reasons" => []
578
+ }
579
+ [t.stopped_reason, c.reason].each { |r|
580
+ next if r.nil?
581
+ containers[c.name][t.desired_status]["reasons"] << r
582
+ }
583
+ containers[c.name][t.desired_status]["reasons"].uniq!
584
+ if !containers[c.name][t.desired_status]['time'] or
585
+ t.created_at > containers[c.name][t.desired_status]['time']
586
+ MU.log c.name, MU::NOTICE, details: t
587
+ containers[c.name][t.desired_status] = {
588
+ "time" => t.created_at,
589
+ "status" => c.last_status,
590
+ "reasons" => containers[c.name][t.desired_status]["reasons"]
591
+ }
592
+ end
593
+ }
594
+ }
595
+ end while tasks.size > 0
596
+
597
+ to_return = true
598
+ containers.each_pair { |name, states|
599
+ if !states["RUNNING"] or states["RUNNING"]["status"] != "RUNNING"
600
+ to_return = false
601
+ if states["STOPPED"] and states["STOPPED"]["status"]
602
+ MU.log "Container #{name} has failures", MU::WARN, details: states["STOPPED"] if log
603
+ elsif states["RUNNING"] and states["RUNNING"]["status"]
604
+ MU.log "Container #{name} not currently running", MU::NOTICE, details: states["RUNNING"] if log
605
+ else
606
+ MU.log "Container #{name} in unknown state", MU::WARN, details: states["STOPPED"] if log
607
+ end
608
+ else
609
+ MU.log "Container #{name} running", details: states["RUNNING"] if log
610
+ end
611
+ }
612
+
613
+ to_return
285
614
  end
286
615
 
287
616
  # Return the cloud layer descriptor for this EKS/ECS/Fargate cluster
@@ -337,7 +666,19 @@ module MU
337
666
  elsif flavor == "EKS"
338
667
  # XXX this is absurd, but these don't appear to be available from an API anywhere
339
668
  # Here's their Packer build, should just convert to Chef: https://github.com/awslabs/amazon-eks-ami
340
- amis = { "us-east-1" => "ami-0440e4f6b9713faf6", "us-west-2" => "ami-0a54c984b9f908c81", "eu-west-1" => "ami-0c7a4976cb6fafd3a" }
669
+ amis = {
670
+ "us-east-1" => "ami-0abcb9f9190e867ab",
671
+ "us-east-2" => "ami-04ea7cb66af82ae4a",
672
+ "us-west-2" => "ami-0923e4b35a30a5f53",
673
+ "eu-west-1" => "ami-08716b70cac884aaa",
674
+ "eu-west-2" => "ami-0c7388116d474ee10",
675
+ "eu-west-3" => "ami-0560aea042fec8b12",
676
+ "ap-northeast-1" => "ami-0bfedee6a7845c26d",
677
+ "ap-northeast-2" => "ami-0a904348b703e620c",
678
+ "ap-south-1" => "ami-09c3eb35bb3be46a4",
679
+ "ap-southeast-1" => "ami-07b922b9b94d9a6d2",
680
+ "ap-southeast-2" => "ami-0f0121e9e64ebd3dc"
681
+ }
341
682
  return amis[region]
342
683
  end
343
684
  nil
@@ -381,6 +722,24 @@ module MU
381
722
  resp.cluster_arns.each { |arn|
382
723
  if arn.match(/:cluster\/(#{MU.deploy_id}[^:]+)$/)
383
724
  cluster = Regexp.last_match[1]
725
+
726
+ svc_resp = MU::Cloud::AWS.ecs(region: region, credentials: credentials).list_services(
727
+ cluster: arn
728
+ )
729
+ if svc_resp and svc_resp.service_arns
730
+ svc_resp.service_arns.each { |svc_arn|
731
+ svc_name = svc_arn.gsub(/.*?:service\/(.*)/, '\1')
732
+ MU.log "Deleting Service #{svc_name} from ECS Cluster #{cluster}"
733
+ if !noop
734
+ MU::Cloud::AWS.ecs(region: region, credentials: credentials).delete_service(
735
+ cluster: arn,
736
+ service: svc_name,
737
+ force: true # man forget scaling up and down if we're just deleting the cluster
738
+ )
739
+ end
740
+ }
741
+ end
742
+
384
743
  instances = MU::Cloud::AWS.ecs(credentials: credentials, region: region).list_container_instances({
385
744
  cluster: cluster
386
745
  })
@@ -400,13 +759,33 @@ module MU
400
759
  MU.log "Deleting ECS Cluster #{cluster}"
401
760
  if !noop
402
761
  # TODO de-register container instances
762
+ begin
403
763
  deletion = MU::Cloud::AWS.ecs(credentials: credentials, region: region).delete_cluster(
404
764
  cluster: cluster
405
765
  )
766
+ rescue Aws::ECS::Errors::ClusterContainsTasksException => e
767
+ sleep 5
768
+ retry
769
+ end
406
770
  end
407
771
  end
408
772
  }
409
773
  end
774
+
775
+ tasks = MU::Cloud::AWS.ecs(region: region, credentials: credentials).list_task_definitions(
776
+ family_prefix: MU.deploy_id
777
+ )
778
+ if tasks and tasks.task_definition_arns
779
+ tasks.task_definition_arns.each { |arn|
780
+ MU.log "Deregistering Fargate task definition #{arn}"
781
+ if !noop
782
+ MU::Cloud::AWS.ecs(region: region, credentials: credentials).deregister_task_definition(
783
+ task_definition: arn
784
+ )
785
+ end
786
+ }
787
+ end
788
+
410
789
  return if !MU::Cloud::AWS::ContainerCluster.EKSRegions.include?(region)
411
790
 
412
791
 
@@ -491,19 +870,553 @@ module MU
491
870
  "enum" => ["ECS", "EKS", "Fargate"],
492
871
  "default" => "ECS"
493
872
  },
873
+ "kubernetes" => {
874
+ "default" => { "version" => "1.11" }
875
+ },
494
876
  "platform" => {
495
- "description" => "The platform to choose for worker nodes. Will default to Amazon Linux for ECS, CentOS 7 for everything else",
877
+ "description" => "The platform to choose for worker nodes. Will default to Amazon Linux for ECS, CentOS 7 for everything else. Only valid for EKS and ECS flavors.",
496
878
  "default" => "centos7"
497
879
  },
498
880
  "ami_id" => {
499
881
  "type" => "string",
500
- "description" => "The Amazon EC2 AMI on which to base this cluster's container hosts. Will use the default appropriate for the platform, if not specified."
882
+ "description" => "The Amazon EC2 AMI on which to base this cluster's container hosts. Will use the default appropriate for the platform, if not specified. Only valid for EKS and ECS flavors."
501
883
  },
502
884
  "run_list" => {
503
885
  "type" => "array",
504
886
  "items" => {
505
887
  "type" => "string",
506
- "description" => "An extra Chef run list entry, e.g. role[rolename] or recipe[recipename]s, to be run on worker nodes."
888
+ "description" => "An extra Chef run list entry, e.g. role[rolename] or recipe[recipename]s, to be run on worker nodes. Only valid for EKS and ECS flavors."
889
+ }
890
+ },
891
+ "ingress_rules" => {
892
+ "type" => "array",
893
+ "items" => MU::Config::FirewallRule.ruleschema,
894
+ "default" => [
895
+ {
896
+ "egress" => true,
897
+ "port" => 443,
898
+ "hosts" => [ "0.0.0.0/0" ]
899
+ }
900
+ ]
901
+ },
902
+ "logging" => {
903
+ "type" => "array",
904
+ "default" => ["authenticator", "api"],
905
+ "items" => {
906
+ "type" => "string",
907
+ "description" => "Cluster CloudWatch logs to enable for EKS clusters.",
908
+ "enum" => ["api", "audit", "authenticator", "controllerManager", "scheduler"]
909
+ }
910
+ },
911
+ "volumes" => {
912
+ "type" => "array",
913
+ "items" => {
914
+ "description" => "Define one or more volumes which can then be referenced by the +mount_points+ parameter inside +containers+. +docker+ volumes are not valid for Fargate clusters. See also https://docs.aws.amazon.com/AmazonECS/latest/developerguide/using_data_volumes.html",
915
+ "type" => "object",
916
+ "required" => ["name", "type"],
917
+ "properties" => {
918
+ "name" => {
919
+ "type" => "string",
920
+ "description" => "Name this volume so it can be referenced by containers."
921
+ },
922
+ "type" => {
923
+ "type" => "string",
924
+ "enum" => ["docker", "host"]
925
+ },
926
+ "docker_volume_configuration" => {
927
+ "type" => "object",
928
+ "default" => {
929
+ "autoprovision" => true,
930
+ "driver" => "local"
931
+ },
932
+ "description" => "This parameter is specified when you are using +docker+ volumes. Docker volumes are only supported when you are using the EC2 launch type. To use bind mounts, specify a +host+ volume instead.",
933
+ "properties" => {
934
+ "autoprovision" => {
935
+ "type" => "boolean",
936
+ "description" => "Create the Docker volume if it does not already exist.",
937
+ "default" => true
938
+ },
939
+ "driver" => {
940
+ "type" => "string",
941
+ "description" => "The Docker volume driver to use. Note that Windows containers can only use the +local+ driver. This parameter maps to +Driver+ in the Create a volume section of the Docker Remote API and the +xxdriver+ option to docker volume create."
942
+ },
943
+ "labels" => {
944
+ "description" => "Custom metadata to add to your Docker volume.",
945
+ "type" => "object"
946
+ },
947
+ "driver_opts" => {
948
+ "description" => "A map of Docker driver-specific options passed through. This parameter maps to +DriverOpts+ in the Create a volume section of the Docker Remote API and the +xxopt+ option to docker volume create .",
949
+ "type" => "object"
950
+ },
951
+ }
952
+ },
953
+ "host_volume_source_path" => {
954
+ "type" => "string",
955
+ "description" => "If specified, and the +type+ of this volume is +host+, data will be stored in the container host in this location and will persist after containers associated with it stop running."
956
+ }
957
+ }
958
+ }
959
+ },
960
+ "containers" => {
961
+ "type" => "array",
962
+ "items" => {
963
+ "type" => "object",
964
+ "description" => "A container image to run on this cluster.",
965
+ "required" => ["name", "image"],
966
+ "properties" => {
967
+ "name" => {
968
+ "type" => "string",
969
+ "description" => "The name of a container. If you are linking multiple containers together in a task definition, the name of one container can be entered in the +links+ of another container to connect the containers. This parameter maps to +name+ in the Create a container section of the Docker Remote API and the +--name+ option to docker run."
970
+ },
971
+ "service" => {
972
+ "type" => "string",
973
+ "description" => "The Service of which this container will be a component. Default behavior, if unspecified, is to create a service with the name of this container definition and assume they map 1:1."
974
+ },
975
+ "image" => {
976
+ "type" => "string",
977
+ "description" => "A Docker image to run, as a shorthand name for a public Dockerhub image or a full URL to a private container repository (+repository-url/image:tag+ or <tt>repository-url/image@digest</tt>). See +repository_credentials+ to specify authentication for a container repository.",
978
+ },
979
+ "cpu" => {
980
+ "type" => "integer",
981
+ "default" => 256,
982
+ "description" => "CPU to allocate for this container/task. This parameter maps to +CpuShares+ in the Create a container section of the Docker Remote API and the +--cpu-shares+ option to docker run. Not all +cpu+ and +memory+ combinations are valid, particularly when using Fargate, see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html"
983
+ },
984
+ "memory" => {
985
+ "type" => "integer",
986
+ "default" => 512,
987
+ "description" => "Hard limit of memory to allocate for this container/task. Not all +cpu+ and +memory+ combinations are valid, particularly when using Fargate, see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html"
988
+ },
989
+ "memory_reservation" => {
990
+ "type" => "integer",
991
+ "default" => 512,
992
+ "description" => "Soft limit of memory to allocate for this container/task. This parameter maps to +MemoryReservation+ in the Create a container section of the Docker Remote API and the +--memory-reservation+ option to docker run."
993
+ },
994
+ "role" => MU::Config::Role.reference,
995
+ "essential" => {
996
+ "type" => "boolean",
997
+ "description" => "Flag this container as essential or non-essential to its parent task. If the container fails and is marked essential, the parent task will also be marked as failed.",
998
+ "default" => true
999
+ },
1000
+ "hostname" => {
1001
+ "type" => "string",
1002
+ "description" => "Set this container's local hostname. If not specified, will inherit the name of the parent task. Not valid for Fargate clusters. This parameter maps to +Hostname+ in the Create a container section of the Docker Remote API and the +--hostname+ option to docker run."
1003
+ },
1004
+ "user" => {
1005
+ "type" => "string",
1006
+ "description" => "The system-level user to use when executing commands inside this container"
1007
+ },
1008
+ "working_directory" => {
1009
+ "type" => "string",
1010
+ "description" => "The working directory in which to run commands inside the container."
1011
+ },
1012
+ "disable_networking" => {
1013
+ "type" => "boolean",
1014
+ "description" => "This parameter maps to +NetworkDisabled+ in the Create a container section of the Docker Remote API."
1015
+ },
1016
+ "privileged" => {
1017
+ "type" => "boolean",
1018
+ "description" => "When this parameter is true, the container is given elevated privileges on the host container instance (similar to the root user). This parameter maps to +Privileged+ in the Create a container section of the Docker Remote API and the +--privileged+ option to docker run. Not valid for Fargate clusters."
1019
+ },
1020
+ "readonly_root_filesystem" => {
1021
+ "type" => "boolean",
1022
+ "description" => "This parameter maps to +ReadonlyRootfs+ in the Create a container section of the Docker Remote API and the +--read-only+ option to docker run."
1023
+ },
1024
+ "interactive" => {
1025
+ "type" => "boolean",
1026
+ "description" => "When this parameter is +true+, this allows you to deploy containerized applications that require +stdin+ or a +tty+ to be allocated. This parameter maps to +OpenStdin+ in the Create a container section of the Docker Remote API and the +--interactive+ option to docker run."
1027
+ },
1028
+ "pseudo_terminal" => {
1029
+ "type" => "boolean",
1030
+ "description" => "When this parameter is true, a TTY is allocated. This parameter maps to +Tty+ in the Create a container section of the Docker Remote API and the +--tty+ option to docker run."
1031
+ },
1032
+ "start_timeout" => {
1033
+ "type" => "integer",
1034
+ "description" => "Time duration to wait before giving up on containers which have been specified with +depends_on+ for this one."
1035
+ },
1036
+ "stop_timeout" => {
1037
+ "type" => "integer",
1038
+ "description" => "Time duration to wait before the container is forcefully killed if it doesn't exit normally on its own."
1039
+ },
1040
+ "links" => {
1041
+ "type" => "array",
1042
+ "items" => {
1043
+ "description" => "The +link+ parameter allows containers to communicate with each other without the need for port mappings. Only supported if the network mode of a task definition is set to +bridge+. The +name:internalName+ construct is analogous to +name:alias+ in Docker links.",
1044
+ "type" => "string"
1045
+ }
1046
+ },
1047
+ "entry_point" => {
1048
+ "type" => "array",
1049
+ "items" => {
1050
+ "type" => "string",
1051
+ "description" => "The entry point that is passed to the container. This parameter maps to +Entrypoint+ in the Create a container section of the Docker Remote API and the +--entrypoint+ option to docker run."
1052
+ }
1053
+ },
1054
+ "command" => {
1055
+ "type" => "array",
1056
+ "items" => {
1057
+ "type" => "string",
1058
+ "description" => "This parameter maps to +Cmd+ in the Create a container section of the Docker Remote API and the +COMMAND+ parameter to docker run."
1059
+ }
1060
+ },
1061
+ "dns_servers" => {
1062
+ "type" => "array",
1063
+ "items" => {
1064
+ "type" => "string",
1065
+ "description" => "A list of DNS servers that are presented to the container. This parameter maps to +Dns+ in the Create a container section of the Docker Remote API and the +--dns+ option to docker run."
1066
+ }
1067
+ },
1068
+ "dns_search_domains" => {
1069
+ "type" => "array",
1070
+ "items" => {
1071
+ "type" => "string",
1072
+ "description" => "A list of DNS search domains that are presented to the container. This parameter maps to +DnsSearch+ in the Create a container section of the Docker Remote API and the +--dns-search+ option to docker run."
1073
+ }
1074
+ },
1075
+ "linux_parameters" => {
1076
+ "type" => "object",
1077
+ "description" => "Linux-specific options that are applied to the container, such as Linux KernelCapabilities.",
1078
+ "properties" => {
1079
+ "init_process_enabled" => {
1080
+ "type" => "boolean",
1081
+ "description" => "Run an +init+ process inside the container that forwards signals and reaps processes. This parameter maps to the +--init+ option to docker run."
1082
+ },
1083
+ "shared_memory_size" => {
1084
+ "type" => "integer",
1085
+ "description" => "The value for the size (in MiB) of the +/dev/shm+ volume. This parameter maps to the +--shm-size+ option to docker run. Not valid for Fargate clusters."
1086
+ },
1087
+ "capabilities" => {
1088
+ "type" => "object",
1089
+ "description" => "The Linux capabilities for the container that are added to or dropped from the default configuration provided by Docker.",
1090
+ "properties" => {
1091
+ "add" => {
1092
+ "type" => "array",
1093
+ "items" => {
1094
+ "type" => "string",
1095
+ "description" => "This parameter maps to +CapAdd+ in the Create a container section of the Docker Remote API and the +--cap-add+ option to docker run. Not valid for Fargate clusters.",
1096
+ "enum" => ["ALL", "AUDIT_CONTROL", "AUDIT_WRITE", "BLOCK_SUSPEND", "CHOWN", "DAC_OVERRIDE", "DAC_READ_SEARCH", "FOWNER", "FSETID", "IPC_LOCK", "IPC_OWNER", "KILL", "LEASE", "LINUX_IMMUTABLE", "MAC_ADMIN", "MAC_OVERRIDE", "MKNOD", "NET_ADMIN", "NET_BIND_SERVICE", "NET_BROADCAST", "NET_RAW", "SETFCAP", "SETGID", "SETPCAP", "SETUID", "SYS_ADMIN", "SYS_BOOT", "SYS_CHROOT", "SYS_MODULE", "SYS_NICE", "SYS_PACCT", "SYS_PTRACE", "SYS_RAWIO", "SYS_RESOURCE", "SYS_TIME", "SYS_TTY_CONFIG", "SYSLOG", "WAKE_ALARM"]
1097
+ }
1098
+ },
1099
+ "drop" => {
1100
+ "type" => "array",
1101
+ "items" => {
1102
+ "type" => "string",
1103
+ "description" => "This parameter maps to +CapDrop+ in the Create a container section of the Docker Remote API and the +--cap-drop+ option to docker run.",
1104
+ "enum" => ["ALL", "AUDIT_CONTROL", "AUDIT_WRITE", "BLOCK_SUSPEND", "CHOWN", "DAC_OVERRIDE", "DAC_READ_SEARCH", "FOWNER", "FSETID", "IPC_LOCK", "IPC_OWNER", "KILL", "LEASE", "LINUX_IMMUTABLE", "MAC_ADMIN", "MAC_OVERRIDE", "MKNOD", "NET_ADMIN", "NET_BIND_SERVICE", "NET_BROADCAST", "NET_RAW", "SETFCAP", "SETGID", "SETPCAP", "SETUID", "SYS_ADMIN", "SYS_BOOT", "SYS_CHROOT", "SYS_MODULE", "SYS_NICE", "SYS_PACCT", "SYS_PTRACE", "SYS_RAWIO", "SYS_RESOURCE", "SYS_TIME", "SYS_TTY_CONFIG", "SYSLOG", "WAKE_ALARM"]
1105
+ }
1106
+ }
1107
+ }
1108
+ },
1109
+ "devices" => {
1110
+ "type" => "array",
1111
+ "items" => {
1112
+ "type" => "object",
1113
+ "description" => "Host devices to expose to the container.",
1114
+ "properties" => {
1115
+ "host_path" => {
1116
+ "type" => "string",
1117
+ "description" => "The path for the device on the host container instance."
1118
+ },
1119
+ "container_path" => {
1120
+ "type" => "string",
1121
+ "description" => "The path inside the container at which to expose the host device."
1122
+ },
1123
+ "permissions" => {
1124
+ "type" => "array",
1125
+ "items" => {
1126
+ "description" => "The explicit permissions to provide to the container for the device. By default, the container has permissions for +read+, +write+, and +mknod+ for the device.",
1127
+ "type" => "string"
1128
+ }
1129
+ }
1130
+ }
1131
+ }
1132
+ },
1133
+ "tmpfs" => {
1134
+ "type" => "array",
1135
+ "items" => {
1136
+ "type" => "object",
1137
+ "description" => "A tmpfs device to expost to the container. This parameter maps to the +--tmpfs+ option to docker run. Not valid for Fargate clusters.",
1138
+ "properties" => {
1139
+ "container_path" => {
1140
+ "type" => "string",
1141
+ "description" => "The absolute file path where the tmpfs volume is to be mounted."
1142
+ },
1143
+ "size" => {
1144
+ "type" => "integer",
1145
+ "description" => "The size (in MiB) of the tmpfs volume."
1146
+ },
1147
+ "mount_options" => {
1148
+ "type" => "array",
1149
+ "items" => {
1150
+ "description" => "tmpfs volume mount options",
1151
+ "type" => "string",
1152
+ "enum" => ["defaults", "ro", "rw", "suid", "nosuid", "dev", "nodev", "exec", "noexec", "sync", "async", "dirsync", "remount", "mand", "nomand", "atime", "noatime", "diratime", "nodiratime", "bind", "rbind", "unbindable", "runbindable", "private", "rprivate", "shared", "rshared", "slave", "rslave", "relatime", "norelatime", "strictatime", "nostrictatime", "mode", "uid", "gid", "nr_inodes", "nr_blocks", "mpol"]
1153
+ }
1154
+ }
1155
+ }
1156
+ }
1157
+ }
1158
+ }
1159
+ },
1160
+ "docker_labels" => {
1161
+ "type" => "object",
1162
+ "description" => "A key/value map of labels to add to the container. This parameter maps to +Labels+ in the Create a container section of the Docker Remote API and the +--label+ option to docker run."
1163
+ },
1164
+ "docker_security_options" => {
1165
+ "type" => "array",
1166
+ "items" => {
1167
+ "type" => "string",
1168
+ "description" => "A list of strings to provide custom labels for SELinux and AppArmor multi-level security systems. This field is not valid for containers in tasks using the Fargate launch type. This parameter maps to +SecurityOpt+ in the Create a container section of the Docker Remote API and the +--security-opt+ option to docker run."
1169
+ }
1170
+ },
1171
+ "health_check" => {
1172
+ "type" => "object",
1173
+ "required" => ["command"],
1174
+ "description" => "The health check command and associated configuration parameters for the container. This parameter maps to +HealthCheck+ in the Create a container section of the Docker Remote API and the +HEALTHCHECK+ parameter of docker run.",
1175
+ "properties" => {
1176
+ "command" => {
1177
+ "type" => "array",
1178
+ "items" => {
1179
+ "type" => "string",
1180
+ "description" => "A string array representing the command that the container runs to determine if it is healthy."
1181
+ }
1182
+ },
1183
+ "interval" => {
1184
+ "type" => "integer",
1185
+ "description" => "The time period in seconds between each health check execution."
1186
+ },
1187
+ "timeout" => {
1188
+ "type" => "integer",
1189
+ "description" => "The time period in seconds to wait for a health check to succeed before it is considered a failure."
1190
+ },
1191
+ "retries" => {
1192
+ "type" => "integer",
1193
+ "description" => "The number of times to retry a failed health check before the container is considered unhealthy."
1194
+ },
1195
+ "start_period" => {
1196
+ "type" => "integer",
1197
+ "description" => "The optional grace period within which to provide containers time to bootstrap before failed health checks count towards the maximum number of retries."
1198
+ }
1199
+ }
1200
+ },
1201
+ "environment" => {
1202
+ "type" => "array",
1203
+ "items" => {
1204
+ "type" => "object",
1205
+ "description" => "The environment variables to pass to a container. This parameter maps to +Env+ in the Create a container section of the Docker Remote API and the +--env+ option to docker run.",
1206
+ "properties" => {
1207
+ "name" => {
1208
+ "type" => "string"
1209
+ },
1210
+ "value" => {
1211
+ "type" => "string"
1212
+ }
1213
+ }
1214
+ }
1215
+ },
1216
+ "resource_requirements" => {
1217
+ "type" => "array",
1218
+ "items" => {
1219
+ "type" => "object",
1220
+ "description" => "Special requirements for this container. As of this writing, +GPU+ is the only valid option.",
1221
+ "required" => ["type", "value"],
1222
+ "properties" => {
1223
+ "type" => {
1224
+ "type" => "string",
1225
+ "enum" => ["GPU"],
1226
+ "description" => "Special requirements for this container. As of this writing, +GPU+ is the only valid option."
1227
+ },
1228
+ "value" => {
1229
+ "type" => "string",
1230
+ "description" => "The number of physical GPUs the Amazon ECS container agent will reserve for the container."
1231
+ }
1232
+ }
1233
+ }
1234
+ },
1235
+ "system_controls" => {
1236
+ "type" => "array",
1237
+ "items" => {
1238
+ "type" => "object",
1239
+ "description" => "A list of namespaced kernel parameters to set in the container. This parameter maps to +Sysctls+ in the Create a container section of the Docker Remote API and the +--sysctl+ option to docker run.",
1240
+ "properties" => {
1241
+ "namespace" => {
1242
+ "type" => "string",
1243
+ "description" => "The namespaced kernel parameter for which to set a +value+."
1244
+ },
1245
+ "value" => {
1246
+ "type" => "string",
1247
+ "description" => "The value for the namespaced kernel parameter specified in +namespace+."
1248
+ }
1249
+ }
1250
+ }
1251
+ },
1252
+ "ulimits" => {
1253
+ "type" => "array",
1254
+ "items" => {
1255
+ "type" => "object",
1256
+ "description" => "This parameter maps to +Ulimits+ in the Create a container section of the Docker Remote API and the +--ulimit+ option to docker run.",
1257
+ "required" => ["name", "soft_limit", "hard_limit"],
1258
+ "properties" => {
1259
+ "name" => {
1260
+ "type" => "string",
1261
+ "description" => "The ulimit parameter to set.",
1262
+ "enum" => ["core", "cpu", "data", "fsize", "locks", "memlock", "msgqueue", "nice", "nofile", "nproc", "rss", "rtprio", "rttime", "sigpending", "stack"]
1263
+ },
1264
+ "soft_limit" => {
1265
+ "type" => "integer",
1266
+ "description" => "The soft limit for the ulimit type."
1267
+ },
1268
+ "hard_limit" => {
1269
+ "type" => "integer",
1270
+ "description" => "The hard limit for the ulimit type."
1271
+ },
1272
+ }
1273
+ }
1274
+ },
1275
+ "extra_hosts" => {
1276
+ "type" => "array",
1277
+ "items" => {
1278
+ "type" => "object",
1279
+ "description" => "A list of hostnames and IP address mappings to append to the +/etc/hosts+ file on the container. This parameter maps to ExtraHosts in the +Create+ a container section of the Docker Remote API and the +--add-host+ option to docker run.",
1280
+ "required" => ["hostname", "ip_address"],
1281
+ "properties" => {
1282
+ "hostname" => {
1283
+ "type" => "string"
1284
+ },
1285
+ "ip_address" => {
1286
+ "type" => "string"
1287
+ }
1288
+ }
1289
+ }
1290
+ },
1291
+ "secrets" => {
1292
+ "type" => "array",
1293
+ "items" => {
1294
+ "type" => "object",
1295
+ "description" => "See https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html",
1296
+ "required" => ["name", "value_from"],
1297
+ "properties" => {
1298
+ "name" => {
1299
+ "type" => "string",
1300
+ "description" => "The value to set as the environment variable on the container."
1301
+ },
1302
+ "value_from" => {
1303
+ "type" => "string",
1304
+ "description" => "The secret to expose to the container."
1305
+ }
1306
+ }
1307
+ }
1308
+ },
1309
+ "depends_on" => {
1310
+ "type" => "array",
1311
+ "items" => {
1312
+ "type" => "object",
1313
+ "required" => ["container_name", "condition"],
1314
+ "description" => "The dependencies defined for container startup and shutdown. A container can contain multiple dependencies. When a dependency is defined for container startup, for container shutdown it is reversed.",
1315
+ "properties" => {
1316
+ "container_name" => {
1317
+ "type" => "string"
1318
+ },
1319
+ "condition" => {
1320
+ "type" => "string",
1321
+ "enum" => ["START", "COMPLETE", "SUCCESS", "HEALTHY"]
1322
+ }
1323
+ }
1324
+ }
1325
+ },
1326
+ "mount_points" => {
1327
+ "type" => "array",
1328
+ "items" => {
1329
+ "type" => "object",
1330
+ "description" => "The mount points for data volumes in your container. This parameter maps to +Volumes+ in the Create a container section of the Docker Remote API and the +--volume+ option to docker run.",
1331
+ "properties" => {
1332
+ "source_volume" => {
1333
+ "type" => "string",
1334
+ "description" => "The name of the +volume+ to mount, defined under the +volumes+ section of our parent +container_cluster+ (if the volume is not defined, an ephemeral bind host volume will be allocated)."
1335
+ },
1336
+ "container_path" => {
1337
+ "type" => "string",
1338
+ "description" => "The container-side path where this volume must be mounted"
1339
+ },
1340
+ "read_only" => {
1341
+ "type" => "boolean",
1342
+ "default" => false,
1343
+ "description" => "Mount the volume read-only"
1344
+ }
1345
+ }
1346
+ }
1347
+ },
1348
+ "volumes_from" => {
1349
+ "type" => "array",
1350
+ "items" => {
1351
+ "type" => "object",
1352
+ "description" => "Data volumes to mount from another container. This parameter maps to +VolumesFrom+ in the Create a container section of the Docker Remote API and the +--volumes-from+ option to docker run.",
1353
+ "properties" => {
1354
+ "source_container" => {
1355
+ "type" => "string",
1356
+ "description" => "The name of another container within the same task definition from which to mount volumes."
1357
+ },
1358
+ "read_only" => {
1359
+ "type" => "boolean",
1360
+ "default" => false,
1361
+ "description" => "If this value is +true+, the container has read-only access to the volume."
1362
+ }
1363
+ }
1364
+ }
1365
+ },
1366
+ "repository_credentials" => {
1367
+ "type" => "object",
1368
+ "description" => "The Amazon Resource Name (ARN) of a secret containing the private repository credentials.",
1369
+ "properties" => {
1370
+ "credentials_parameter" => {
1371
+ "type" => "string",
1372
+ # XXX KMS? Secrets Manager? This documentation is vague.
1373
+ "description" => "The Amazon Resource Name (ARN) of a secret containing the private repository credentials."
1374
+ }
1375
+ }
1376
+ },
1377
+ "port_mappings" => {
1378
+ "type" => "array",
1379
+ "items" => {
1380
+ "description" => "Mappings of ports between the container instance and the host instance. This parameter maps to +PortBindings+ in the Create a container section of the Docker Remote API and the +--publish+ option to docker run.",
1381
+ "type" => "object",
1382
+ "properties" => {
1383
+ "container_port" => {
1384
+ "type" => "integer",
1385
+ "description" => "The port number on the container that is bound to the user-specified or automatically assigned host port."
1386
+ },
1387
+ "host_port" => {
1388
+ "type" => "integer",
1389
+ "description" => "The port number on the container instance to reserve for your container. This should not be specified for Fargate clusters, nor for ECS clusters deployed into VPCs."
1390
+ },
1391
+ "protocol" => {
1392
+ "type" => "string",
1393
+ "description" => "The protocol used for the port mapping.",
1394
+ "enum" => ["tcp", "udp"],
1395
+ "default" => "tcp"
1396
+ },
1397
+ }
1398
+ }
1399
+ },
1400
+ "log_configuration" => {
1401
+ "type" => "object",
1402
+ "description" => "Where to send container logs. If not specified, Mu will create a CloudWatch Logs output channel. See also: https://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Types/ContainerDefinition.html#log_configuration-instance_method",
1403
+ "default" => {
1404
+ "log_driver" => "awslogs"
1405
+ },
1406
+ "required" => ["log_driver"],
1407
+ "properties" => {
1408
+ "log_driver" => {
1409
+ "type" => "string",
1410
+ "description" => "Type of logging facility to use for container logs.",
1411
+ "enum" => ["json-file", "syslog", "journald", "gelf", "fluentd", "awslogs", "splunk"]
1412
+ },
1413
+ "options" => {
1414
+ "type" => "object",
1415
+ "description" => "Per-driver configuration options. See also: https://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Types/ContainerDefinition.html#log_configuration-instance_method"
1416
+ }
1417
+ }
1418
+ }
1419
+ }
507
1420
  }
508
1421
  }
509
1422
  }
@@ -520,7 +1433,6 @@ module MU
520
1433
  cluster['size'] = MU::Cloud::AWS::Server.validateInstanceType(cluster["instance_type"], cluster["region"])
521
1434
  ok = false if cluster['size'].nil?
522
1435
 
523
-
524
1436
  if cluster["flavor"] == "ECS" and cluster["kubernetes"] and !MU::Cloud::AWS.isGovCloud?(cluster["region"])
525
1437
  cluster["flavor"] = "EKS"
526
1438
  MU.log "Setting flavor of ContainerCluster '#{cluster['name']}' to EKS ('kubernetes' stanza was specified)", MU::NOTICE
@@ -531,8 +1443,101 @@ module MU
531
1443
  ok = false
532
1444
  end
533
1445
 
534
- if MU::Cloud::AWS.isGovCloud?(cluster["region"]) and cluster["flavor"] != "ECS"
535
- MU.log "AWS GovCloud does not support #{cluster["flavor"]} yet, just ECS", MU::ERR
1446
+ if cluster["volumes"]
1447
+ cluster["volumes"].each { |v|
1448
+ if v["type"] == "docker"
1449
+ if cluster["flavor"] == "Fargate"
1450
+ MU.log "ContainerCluster #{cluster['name']}: Docker volumes are not supported in Fargate clusters (volume '#{v['name']}' is not valid)", MU::ERR
1451
+ ok = false
1452
+ end
1453
+ end
1454
+ }
1455
+ end
1456
+
1457
+ if cluster["flavor"] != "EKS" and cluster["containers"]
1458
+ created_generic_loggroup = false
1459
+ cluster['containers'].each { |c|
1460
+ if c['log_configuration'] and
1461
+ c['log_configuration']['log_driver'] == "awslogs" and
1462
+ (!c['log_configuration']['options'] or !c['log_configuration']['options']['awslogs-group'])
1463
+
1464
+ logname = cluster["name"]+"-svclogs"
1465
+ rolename = cluster["name"]+"-logrole"
1466
+ c['log_configuration']['options'] ||= {}
1467
+ c['log_configuration']['options']['awslogs-group'] = logname
1468
+ c['log_configuration']['options']['awslogs-region'] = cluster["region"]
1469
+ c['log_configuration']['options']['awslogs-stream-prefix'] ||= c['name']
1470
+ if c['mount_points']
1471
+ cluster['volumes'] ||= []
1472
+ volnames = cluster['volumes'].map { |v| v['name'] }
1473
+ c['mount_points'].each { |m|
1474
+ if !volnames.include?(m['source_volume'])
1475
+ cluster['volumes'] << {
1476
+ "name" => m['source_volume'],
1477
+ "type" => "host"
1478
+ }
1479
+ end
1480
+ }
1481
+ end
1482
+
1483
+ if !created_generic_loggroup
1484
+ cluster["dependencies"] << { "type" => "log", "name" => logname }
1485
+ logdesc = {
1486
+ "name" => logname,
1487
+ "region" => cluster["region"],
1488
+ "cloud" => cluster["cloud"]
1489
+ }
1490
+ configurator.insertKitten(logdesc, "logs")
1491
+
1492
+ if !c['role']
1493
+ roledesc = {
1494
+ "name" => rolename,
1495
+ "cloud" => cluster["cloud"],
1496
+ "can_assume" => [
1497
+ {
1498
+ "entity_id" => "ecs-tasks.amazonaws.com",
1499
+ "entity_type" => "service"
1500
+ }
1501
+ ],
1502
+ "policies" => [
1503
+ {
1504
+ "name" => "ECSTaskLogPerms",
1505
+ "permissions" => [
1506
+ "logs:CreateLogStream",
1507
+ "logs:DescribeLogGroups",
1508
+ "logs:DescribeLogStreams",
1509
+ "logs:PutLogEvents"
1510
+ ],
1511
+ "import" => [
1512
+ ""
1513
+ ],
1514
+ "targets" => [
1515
+ {
1516
+ "type" => "log",
1517
+ "identifier" => logname
1518
+ }
1519
+ ]
1520
+ }
1521
+ ],
1522
+ "dependencies" => [{ "type" => "log", "name" => logname }]
1523
+ }
1524
+ configurator.insertKitten(roledesc, "roles")
1525
+
1526
+ cluster["dependencies"] << {
1527
+ "type" => "role",
1528
+ "name" => rolename
1529
+ }
1530
+ end
1531
+
1532
+ created_generic_loggroup = true
1533
+ end
1534
+ c['role'] ||= { 'name' => rolename }
1535
+ end
1536
+ }
1537
+ end
1538
+
1539
+ if MU::Cloud::AWS.isGovCloud?(cluster["region"]) and cluster["flavor"] == "EKS"
1540
+ MU.log "AWS GovCloud does not support #{cluster["flavor"]} yet", MU::ERR
536
1541
  ok = false
537
1542
  end
538
1543
 
@@ -563,6 +1568,46 @@ module MU
563
1568
  end
564
1569
  end
565
1570
 
1571
+ if cluster["flavor"] == "Fargate" and !cluster['vpc']
1572
+ if MU.myVPC
1573
+ cluster["vpc"] = {
1574
+ "vpc_id" => MU.myVPC,
1575
+ "subnet_pref" => "all_private"
1576
+ }
1577
+ MU.log "Fargate cluster #{cluster['name']} did not specify a VPC, inserting into private subnets of #{MU.myVPC}", MU::NOTICE
1578
+ else
1579
+ MU.log "Fargate cluster #{cluster['name']} must specify a VPC", MU::ERR
1580
+ ok = false
1581
+ end
1582
+
1583
+ end
1584
+
1585
+ cluster['ingress_rules'] ||= []
1586
+ if cluster['flavor'] == "ECS"
1587
+ cluster['ingress_rules'] << {
1588
+ "sgs" => ["server_pool#{cluster['name']}workers"],
1589
+ "port" => 443
1590
+ }
1591
+ end
1592
+ fwname = "container_cluster#{cluster['name']}"
1593
+
1594
+ acl = {
1595
+ "name" => fwname,
1596
+ "credentials" => cluster["credentials"],
1597
+ "rules" => cluster['ingress_rules'],
1598
+ "region" => cluster['region'],
1599
+ "optional_tags" => cluster['optional_tags']
1600
+ }
1601
+ acl["tags"] = cluster['tags'] if cluster['tags'] && !cluster['tags'].empty?
1602
+ acl["vpc"] = cluster['vpc'].dup if cluster['vpc']
1603
+
1604
+ ok = false if !configurator.insertKitten(acl, "firewall_rules")
1605
+ cluster["add_firewall_rules"] = [] if cluster["add_firewall_rules"].nil?
1606
+ cluster["add_firewall_rules"] << {"rule_name" => fwname}
1607
+ cluster["dependencies"] << {
1608
+ "name" => fwname,
1609
+ "type" => "firewall_rule",
1610
+ }
566
1611
 
567
1612
  if ["ECS", "EKS"].include?(cluster["flavor"])
568
1613
 
@@ -574,6 +1619,7 @@ module MU
574
1619
  "max_size" => cluster["instance_count"],
575
1620
  "wait_for_nodes" => cluster["instance_count"],
576
1621
  "ssh_user" => cluster["host_ssh_user"],
1622
+ "role_strip_path" => true,
577
1623
  "basis" => {
578
1624
  "launch_config" => {
579
1625
  "name" => cluster["name"]+"workers",
@@ -594,7 +1640,8 @@ module MU
594
1640
  worker_pool["vpc"] = cluster["vpc"].dup
595
1641
  worker_pool["vpc"]["subnet_pref"] = cluster["instance_subnet_pref"]
596
1642
  worker_pool["vpc"].delete("subnets")
597
- end
1643
+ end
1644
+
598
1645
  if cluster["host_image"]
599
1646
  worker_pool["basis"]["launch_config"]["image_id"] = cluster["host_image"]
600
1647
  end
@@ -628,32 +1675,9 @@ module MU
628
1675
  "name" => cluster["name"]+"workers",
629
1676
  "type" => "server_pool",
630
1677
  }
631
- elsif cluster["flavor"] == "EKS"
632
- cluster['ingress_rules'] ||= []
633
- cluster['ingress_rules'] << {
634
- "sgs" => ["server_pool#{cluster['name']}workers"],
635
- "port" => 443
636
- }
637
- fwname = "container_cluster#{cluster['name']}"
638
-
639
- acl = {
640
- "name" => fwname,
641
- "credentials" => cluster["credentials"],
642
- "rules" => cluster['ingress_rules'],
643
- "region" => cluster['region'],
644
- "optional_tags" => cluster['optional_tags']
645
- }
646
- acl["tags"] = cluster['tags'] if cluster['tags'] && !cluster['tags'].empty?
647
- acl["vpc"] = cluster['vpc'].dup if cluster['vpc']
648
-
649
- ok = false if !configurator.insertKitten(acl, "firewall_rules")
650
- cluster["add_firewall_rules"] = [] if cluster["add_firewall_rules"].nil?
651
- cluster["add_firewall_rules"] << {"rule_name" => fwname}
652
- cluster["dependencies"] << {
653
- "name" => fwname,
654
- "type" => "firewall_rule",
655
- }
1678
+ end
656
1679
 
1680
+ if cluster["flavor"] == "EKS"
657
1681
  role = {
658
1682
  "name" => cluster["name"]+"controlplane",
659
1683
  "credentials" => cluster["credentials"],