ecs_deploy_cli 0.1.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ecs_deploy_cli.rb +5 -1
  3. data/lib/ecs_deploy_cli/cli.rb +40 -2
  4. data/lib/ecs_deploy_cli/cloudformation/default.yml +411 -0
  5. data/lib/ecs_deploy_cli/dsl/auto_options.rb +17 -1
  6. data/lib/ecs_deploy_cli/dsl/cluster.rb +70 -0
  7. data/lib/ecs_deploy_cli/dsl/container.rb +8 -10
  8. data/lib/ecs_deploy_cli/dsl/parser.rb +9 -5
  9. data/lib/ecs_deploy_cli/dsl/service.rb +31 -2
  10. data/lib/ecs_deploy_cli/dsl/task.rb +2 -0
  11. data/lib/ecs_deploy_cli/runner.rb +26 -125
  12. data/lib/ecs_deploy_cli/runners/base.rb +105 -0
  13. data/lib/ecs_deploy_cli/runners/diff.rb +74 -0
  14. data/lib/ecs_deploy_cli/runners/logs.rb +14 -0
  15. data/lib/ecs_deploy_cli/runners/run_task.rb +27 -0
  16. data/lib/ecs_deploy_cli/runners/setup.rb +128 -0
  17. data/lib/ecs_deploy_cli/runners/ssh.rb +79 -0
  18. data/lib/ecs_deploy_cli/runners/status.rb +23 -0
  19. data/lib/ecs_deploy_cli/runners/update_crons.rb +41 -0
  20. data/lib/ecs_deploy_cli/runners/update_services.rb +55 -0
  21. data/lib/ecs_deploy_cli/runners/validate.rb +47 -0
  22. data/lib/ecs_deploy_cli/version.rb +3 -1
  23. data/spec/ecs_deploy_cli/cli_spec.rb +44 -4
  24. data/spec/ecs_deploy_cli/dsl/cluster_spec.rb +48 -0
  25. data/spec/ecs_deploy_cli/dsl/container_spec.rb +6 -4
  26. data/spec/ecs_deploy_cli/dsl/cron_spec.rb +2 -0
  27. data/spec/ecs_deploy_cli/dsl/parser_spec.rb +2 -0
  28. data/spec/ecs_deploy_cli/dsl/service_spec.rb +31 -0
  29. data/spec/ecs_deploy_cli/dsl/task_spec.rb +2 -0
  30. data/spec/ecs_deploy_cli/runner_spec.rb +177 -26
  31. data/spec/ecs_deploy_cli/runners/base_spec.rb +57 -0
  32. data/spec/spec_helper.rb +2 -3
  33. data/spec/support/ECSFile +13 -1
  34. data/spec/support/ECSFile.minimal +12 -0
  35. metadata +104 -13
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EcsDeployCli
4
+ module Runners
5
+ class Diff < Base
6
+ def run!
7
+ require 'hashdiff'
8
+ require 'colorize'
9
+
10
+ _, tasks, = @parser.resolve
11
+
12
+ tasks.each do |task_name, definition|
13
+ EcsDeployCli.logger.info '---'
14
+ EcsDeployCli.logger.info "Task: #{task_name}"
15
+
16
+ result = ecs_client.describe_task_definition(task_definition: task_name).to_h
17
+
18
+ current = cleanup_source_task(result[:task_definition])
19
+ definition = cleanup_source_task(definition)
20
+
21
+ print_diff Hashdiff.diff(current.except(:container_definitions), definition.except(:container_definitions))
22
+
23
+ diff_container_definitions(
24
+ current[:container_definitions],
25
+ definition[:container_definitions]
26
+ )
27
+
28
+ EcsDeployCli.logger.info '---'
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def diff_container_definitions(first, second)
35
+ first.zip(second).each do |a, b|
36
+ EcsDeployCli.logger.info "Container #{a&.dig(:name) || 'NONE'} <=> #{b&.dig(:name) || 'NONE'}"
37
+
38
+ next if !a || !b
39
+
40
+ sort_envs! a
41
+ sort_envs! b
42
+
43
+ print_diff Hashdiff.diff(a.delete_if { |_, v| v.nil? }, b.delete_if { |_, v| v.nil? })
44
+ end
45
+ end
46
+
47
+ def sort_envs!(definition)
48
+ return unless definition[:environment]
49
+
50
+ definition[:environment].sort_by! { |e| e[:name] }
51
+ end
52
+
53
+ def cleanup_source_task(task)
54
+ task.except(
55
+ :revision, :compatibilities, :status, :registered_at, :registered_by,
56
+ :requires_attributes, :task_definition_arn
57
+ ).delete_if { |_, v| v.nil? }
58
+ end
59
+
60
+ def print_diff(diff)
61
+ diff.each do |(op, path, *values)|
62
+ case op
63
+ when '-'
64
+ EcsDeployCli.logger.info "#{op} #{path} => #{values.join(' ')}".colorize(:red)
65
+ when '+'
66
+ EcsDeployCli.logger.info "#{op} #{path} => #{values.join(' ')}".colorize(:green)
67
+ else
68
+ EcsDeployCli.logger.info "#{op} #{path} => #{values.join(' ')}".colorize(:yellow)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EcsDeployCli
4
+ module Runners
5
+ class Logs < Base
6
+ def run!
7
+ # _, tasks, = @parser.resolve
8
+
9
+ # tasks.
10
+ # cwl_client.
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EcsDeployCli
4
+ module Runners
5
+ class RunTask < Base
6
+ def run!(task, launch_type:, security_groups:, subnets:)
7
+ _, tasks, = @parser.resolve
8
+
9
+ task_definition = _update_task tasks[task]
10
+ task_name = "#{task_definition[:family]}:#{task_definition[:revision]}"
11
+
12
+ ecs_client.run_task(
13
+ cluster: config[:cluster],
14
+ task_definition: task_name,
15
+ network_configuration: {
16
+ awsvpc_configuration: {
17
+ subnets: subnets,
18
+ security_groups: security_groups,
19
+ assign_public_ip: 'ENABLED'
20
+ }
21
+ },
22
+ launch_type: launch_type
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EcsDeployCli
4
+ module Runners
5
+ class Setup < Base
6
+ def run!
7
+ services, resolved_tasks, _, cluster_options = @parser.resolve
8
+
9
+ setup_cluster! cluster_options
10
+ setup_services! services, resolved_tasks: resolved_tasks
11
+ end
12
+
13
+ private
14
+
15
+ def setup_cluster!(cluster_options)
16
+ clusters = ecs_client.describe_clusters(clusters: [config[:cluster]]).to_h[:clusters]
17
+ if clusters.length == 1
18
+ EcsDeployCli.logger.info 'Cluster already created, skipping.'
19
+ return
20
+ end
21
+
22
+ EcsDeployCli.logger.info "Creating cluster #{config[:cluster]}..."
23
+
24
+ params = create_params(cluster_options)
25
+
26
+ ecs_client.create_cluster(
27
+ cluster_name: config[:cluster]
28
+ )
29
+
30
+ stack_name = "EC2ContainerService-#{config[:cluster]}"
31
+
32
+
33
+ cf_client.create_stack(
34
+ stack_name: stack_name,
35
+ template_body: File.read(File.join(__dir__, '..', 'cloudformation', 'default.yml')),
36
+ on_failure: 'ROLLBACK',
37
+ parameters: format_cloudformation_params(params)
38
+ )
39
+
40
+ cf_client.wait_until(:stack_create_complete, { stack_name: stack_name }, delay: 30, max_attempts: 120)
41
+ EcsDeployCli.logger.info "Cluster #{config[:cluster]} created!"
42
+ end
43
+
44
+ def setup_services!(services, resolved_tasks:)
45
+ services.each do |service_name, service_definition|
46
+ if ecs_client.describe_services(cluster: config[:cluster], services: [service_name]).to_h[:services].any?
47
+ EcsDeployCli.logger.info "Service #{service_name} already created, skipping."
48
+ next
49
+ end
50
+
51
+ EcsDeployCli.logger.info "Creating service #{service_name}..."
52
+ task_definition = _update_task resolved_tasks[service_definition.options[:task]]
53
+ task_name = "#{task_definition[:family]}:#{task_definition[:revision]}"
54
+
55
+ ecs_client.create_service(
56
+ cluster: config[:cluster],
57
+ desired_count: 1,
58
+ load_balancers: service_definition.as_definition(task_definition)[:load_balancers],
59
+ service_name: service_name,
60
+ task_definition: task_name
61
+ )
62
+ EcsDeployCli.logger.info "Service #{service_name} created!"
63
+ end
64
+ end
65
+
66
+ def create_params(cluster_options)
67
+ raise ArgumentError, 'Missing vpc configuration' unless cluster_options[:vpc]
68
+
69
+ {
70
+ 'AsgMaxSize' => cluster_options[:instances_count],
71
+ 'AutoAssignPublicIp' => 'INHERIT',
72
+ 'ConfigureDataVolume' => false,
73
+ 'ConfigureRootVolume' => true,
74
+ 'DeviceName' => cluster_options[:device_name],
75
+ 'EbsVolumeSize' => cluster_options[:ebs_volume_size],
76
+ 'EbsVolumeType' => cluster_options[:ebs_volume_type],
77
+ 'EcsAmiId' => load_ami_id,
78
+ 'EcsClusterName' => config[:cluster],
79
+ 'EcsEndpoint' => nil,
80
+ 'EcsInstanceType' => cluster_options[:instance_type],
81
+ 'IamRoleInstanceProfile' => "arn:aws:iam::#{config[:aws_profile_id]}:instance-profile/ecsInstanceRole",
82
+ 'IamSpotFleetRoleArn' => nil,
83
+ 'IsWindows' => false,
84
+ 'KeyName' => cluster_options[:keypair_name],
85
+ 'RootDeviceName' => cluster_options[:root_device_name],
86
+ 'RootEbsVolumeSize' => cluster_options[:root_ebs_volume_size],
87
+
88
+ ##### TODO: Implement this feature
89
+ 'SecurityGroupId' => nil,
90
+ 'SecurityIngressCidrIp' => '0.0.0.0/0',
91
+ 'SecurityIngressFromPort' => 80,
92
+ 'SecurityIngressToPort' => 80,
93
+ #####
94
+
95
+ ##### TODO: Implement this feature
96
+ 'SpotAllocationStrategy' => 'diversified',
97
+ 'SpotPrice' => nil,
98
+ 'UseSpot' => false,
99
+ #####
100
+
101
+ 'UserData' => "#!/bin/bash\necho ECS_CLUSTER=#{config[:cluster]} >> /etc/ecs/ecs.config;echo ECS_BACKEND_HOST= >> /etc/ecs/ecs.config;",
102
+ 'VpcAvailabilityZones' => cluster_options.dig(:vpc, :availability_zones),
103
+ 'VpcCidr' => cluster_options.dig(:vpc, :cidr),
104
+ 'SubnetCidr1' => cluster_options.dig(:vpc, :subnet1),
105
+ 'SubnetCidr2' => cluster_options.dig(:vpc, :subnet2),
106
+ 'SubnetCidr3' => cluster_options.dig(:vpc, :subnet3),
107
+
108
+ 'VpcId' => cluster_options.dig(:vpc, :id),
109
+ 'SubnetIds' => cluster_options.dig(:vpc, :subnet_ids)
110
+ }
111
+ end
112
+
113
+ def format_cloudformation_params(params)
114
+ params.map { |k, v| { parameter_key: k, parameter_value: v.to_s } }
115
+ end
116
+
117
+ def load_ami_id
118
+ ami_data = ssm_client.get_parameter(
119
+ name: '/aws/service/ecs/optimized-ami/amazon-linux-2/recommended'
120
+ ).to_h[:parameter]
121
+
122
+ ami_details = JSON.parse(ami_data[:value]).with_indifferent_access
123
+
124
+ ami_details[:image_id]
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EcsDeployCli
4
+ module Runners
5
+ class SSH < Base
6
+ def run!(params = {})
7
+ instance_ids = load_container_instances(params)
8
+
9
+ instance_id = choose_instance_id(instance_ids)
10
+ dns_name = load_dns_name_from_instance_id(instance_id)
11
+ run_ssh(dns_name)
12
+ end
13
+
14
+ private
15
+
16
+ def choose_instance_id(instance_ids)
17
+ raise 'No instance found' if instance_ids.empty?
18
+ return instance_ids[0] if instance_ids.length == 1
19
+
20
+ instances_selection_text = instance_ids.map.with_index do |instance, index|
21
+ "#{index + 1}) #{instance}"
22
+ end.join("\n")
23
+
24
+ EcsDeployCli.logger.info(
25
+ "Found #{instance_ids.count} instances:\n#{instances_selection_text}\nSelect which one you want to access:"
26
+ )
27
+
28
+ index = select_index_from_array(instance_ids, retry_message: 'Invalid option. Select which one you want to access:')
29
+
30
+ instance_ids[index]
31
+ end
32
+
33
+ def select_index_from_array(array, retry_message:)
34
+ while (index = STDIN.gets.chomp)
35
+ if index =~ /\A[1-9][0-9]*\Z/ && (index.to_i - 1) < array.count
36
+ index = index.to_i - 1
37
+ break
38
+ end
39
+
40
+ EcsDeployCli.logger.info(retry_message)
41
+ end
42
+ index
43
+ end
44
+
45
+ def load_dns_name_from_instance_id(instance_id)
46
+ response = ec2_client.describe_instances(
47
+ instance_ids: [instance_id]
48
+ )
49
+
50
+ response.reservations[0].instances[0].public_dns_name
51
+ end
52
+
53
+ def load_container_instances(params = {})
54
+ task_arns = ecs_client.list_tasks(
55
+ **params.merge(cluster: config[:cluster])
56
+ ).to_h[:task_arns]
57
+
58
+ tasks = ecs_client.describe_tasks(
59
+ tasks: task_arns, cluster: config[:cluster]
60
+ ).to_h[:tasks]
61
+
62
+ instances = tasks.map { |task| task[:container_instance_arn] }.uniq
63
+ response = ecs_client.describe_container_instances(
64
+ cluster: config[:cluster],
65
+ container_instances: instances
66
+ )
67
+
68
+ response.container_instances.map(&:ec2_instance_id)
69
+ end
70
+
71
+ def run_ssh(dns_name)
72
+ EcsDeployCli.logger.info "Connecting to ec2-user@#{dns_name}..."
73
+
74
+ Process.fork { exec("ssh ec2-user@#{dns_name}") }
75
+ Process.wait
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EcsDeployCli
4
+ module Runners
5
+ class Status < Base
6
+ def run!(service)
7
+ services, = @parser.resolve
8
+
9
+ services.each do |service_name, service_definition|
10
+ next if !service.nil? && service != service_name
11
+
12
+ # task_definition = _update_task resolved_tasks[service_definition.options[:task]]
13
+ # task_name = "#{task_definition[:family]}:#{task_definition[:revision]}"
14
+
15
+ puts ecs_client.describe_service(
16
+ cluster: config[:cluster],
17
+ service: service_name
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EcsDeployCli
4
+ module Runners
5
+ class UpdateCrons < Base
6
+ def run!
7
+ _, tasks, crons = @parser.resolve
8
+
9
+ crons.each do |cron_name, cron_definition|
10
+ task_definition = tasks[cron_definition[:task_name]]
11
+ raise "Undefined task #{cron_definition[:task_name].inspect} in (#{tasks.keys.inspect})" unless task_definition
12
+
13
+ updated_task = _update_task(task_definition)
14
+
15
+ current_target = cwe_client.list_targets_by_rule(
16
+ {
17
+ rule: cron_name,
18
+ limit: 1
19
+ }
20
+ ).to_h[:targets].first
21
+
22
+ cwe_client.put_rule(
23
+ cron_definition[:rule]
24
+ )
25
+
26
+ cwe_client.put_targets(
27
+ rule: cron_name,
28
+ targets: [
29
+ id: current_target[:id],
30
+ arn: current_target[:arn],
31
+ role_arn: current_target[:role_arn],
32
+ input: cron_definition[:input].to_json,
33
+ ecs_parameters: cron_definition[:ecs_parameters].merge(task_definition_arn: updated_task[:task_definition_arn])
34
+ ]
35
+ )
36
+ EcsDeployCli.logger.info "Deployed scheduled task \"#{cron_name}\"!"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EcsDeployCli
4
+ module Runners
5
+ class UpdateServices < Base
6
+ def run!(service: nil, timeout: 500)
7
+ services, resolved_tasks = @parser.resolve
8
+
9
+ services.each do |service_name, service_definition|
10
+ next if !service.nil? && service != service_name
11
+
12
+ task_definition = _update_task resolved_tasks[service_definition.options[:task]]
13
+ task_name = "#{task_definition[:family]}:#{task_definition[:revision]}"
14
+
15
+ ecs_client.update_service(
16
+ cluster: config[:cluster],
17
+ service: service_name,
18
+ task_definition: task_name
19
+ )
20
+ wait_for_deploy(service_name, task_name, timeout: timeout)
21
+ EcsDeployCli.logger.info "Deployed service \"#{service_name}\"!"
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def wait_for_deploy(service_name, task_name, timeout:)
28
+ wait_data = { cluster: config[:cluster], services: [service_name] }
29
+
30
+ started_at = Time.now
31
+ ecs_client.wait_until(
32
+ :services_stable, wait_data,
33
+ max_attempts: nil,
34
+ before_wait: lambda { |_, response|
35
+ deployments = response.services.first.deployments
36
+ log_deployments task_name, deployments
37
+
38
+ throw :success if deployments.count == 1 && deployments[0].task_definition.end_with?(task_name)
39
+ throw :failure if Time.now - started_at > timeout
40
+ }
41
+ )
42
+ end
43
+
44
+ def log_deployments(task_name, deployments)
45
+ EcsDeployCli.logger.info "Waiting for task: #{task_name} to become ok."
46
+ EcsDeployCli.logger.info 'Deployment status:'
47
+ deployments.each do |deploy|
48
+ EcsDeployCli.logger.info "[#{deploy.status}] task=#{deploy.task_definition.split('/').last}, "\
49
+ "desired_count=#{deploy.desired_count}, pending_count=#{deploy.pending_count}, running_count=#{deploy.running_count}, failed_tasks=#{deploy.failed_tasks}"
50
+ end
51
+ EcsDeployCli.logger.info ''
52
+ end
53
+ end
54
+ end
55
+ end