ecs_deploy_cli 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|