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.
- checksums.yaml +4 -4
- data/lib/ecs_deploy_cli.rb +5 -1
- data/lib/ecs_deploy_cli/cli.rb +40 -2
- data/lib/ecs_deploy_cli/cloudformation/default.yml +411 -0
- data/lib/ecs_deploy_cli/dsl/auto_options.rb +17 -1
- data/lib/ecs_deploy_cli/dsl/cluster.rb +70 -0
- data/lib/ecs_deploy_cli/dsl/container.rb +8 -10
- data/lib/ecs_deploy_cli/dsl/parser.rb +9 -5
- data/lib/ecs_deploy_cli/dsl/service.rb +31 -2
- data/lib/ecs_deploy_cli/dsl/task.rb +2 -0
- data/lib/ecs_deploy_cli/runner.rb +26 -125
- data/lib/ecs_deploy_cli/runners/base.rb +105 -0
- data/lib/ecs_deploy_cli/runners/diff.rb +74 -0
- data/lib/ecs_deploy_cli/runners/logs.rb +14 -0
- data/lib/ecs_deploy_cli/runners/run_task.rb +27 -0
- data/lib/ecs_deploy_cli/runners/setup.rb +128 -0
- data/lib/ecs_deploy_cli/runners/ssh.rb +79 -0
- data/lib/ecs_deploy_cli/runners/status.rb +23 -0
- data/lib/ecs_deploy_cli/runners/update_crons.rb +41 -0
- data/lib/ecs_deploy_cli/runners/update_services.rb +55 -0
- data/lib/ecs_deploy_cli/runners/validate.rb +47 -0
- data/lib/ecs_deploy_cli/version.rb +3 -1
- data/spec/ecs_deploy_cli/cli_spec.rb +44 -4
- data/spec/ecs_deploy_cli/dsl/cluster_spec.rb +48 -0
- data/spec/ecs_deploy_cli/dsl/container_spec.rb +6 -4
- data/spec/ecs_deploy_cli/dsl/cron_spec.rb +2 -0
- data/spec/ecs_deploy_cli/dsl/parser_spec.rb +2 -0
- data/spec/ecs_deploy_cli/dsl/service_spec.rb +31 -0
- data/spec/ecs_deploy_cli/dsl/task_spec.rb +2 -0
- data/spec/ecs_deploy_cli/runner_spec.rb +177 -26
- data/spec/ecs_deploy_cli/runners/base_spec.rb +57 -0
- data/spec/spec_helper.rb +2 -3
- data/spec/support/ECSFile +13 -1
- data/spec/support/ECSFile.minimal +12 -0
- 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,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
|