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
@@ -1,9 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module EcsDeployCli
|
2
4
|
module DSL
|
3
5
|
module AutoOptions
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def allowed_options(*value)
|
10
|
+
@allowed_options = value
|
11
|
+
end
|
12
|
+
|
13
|
+
def _allowed_options
|
14
|
+
@allowed_options ||= []
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
4
18
|
def method_missing(name, *args, &block)
|
5
19
|
if args.count == 1 && !block
|
6
|
-
|
20
|
+
unless self.class._allowed_options.include?(name)
|
21
|
+
EcsDeployCli.logger.info("Used unhandled option #{name.to_sym} = #{args.first} in #{self.class.name}")
|
22
|
+
end
|
7
23
|
_options[name.to_sym] = args.first
|
8
24
|
else
|
9
25
|
super
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EcsDeployCli
|
4
|
+
module DSL
|
5
|
+
class Cluster
|
6
|
+
include AutoOptions
|
7
|
+
|
8
|
+
allowed_options :instances_count, :instance_type, :ebs_volume_size, :keypair_name
|
9
|
+
|
10
|
+
def initialize(name, config)
|
11
|
+
@config = config
|
12
|
+
_options[:name] = name.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def vpc(id = nil, &block)
|
16
|
+
@vpc = VPC.new(id)
|
17
|
+
@vpc.instance_exec(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def as_definition
|
21
|
+
{
|
22
|
+
instances_count: 1,
|
23
|
+
|
24
|
+
device_name: '/dev/xvda',
|
25
|
+
ebs_volume_size: 22,
|
26
|
+
ebs_volume_type: 'gp2',
|
27
|
+
|
28
|
+
root_device_name: '/dev/xvdcz',
|
29
|
+
root_ebs_volume_size: 30,
|
30
|
+
|
31
|
+
vpc: @vpc&.as_definition
|
32
|
+
}.merge(_options)
|
33
|
+
end
|
34
|
+
|
35
|
+
class VPC
|
36
|
+
include AutoOptions
|
37
|
+
allowed_options :cidr, :subnet1, :subnet2, :subnet3
|
38
|
+
|
39
|
+
def initialize(id)
|
40
|
+
_options[:id] = id
|
41
|
+
end
|
42
|
+
|
43
|
+
def availability_zones(*values)
|
44
|
+
_options[:availability_zones] = values.join(',')
|
45
|
+
end
|
46
|
+
|
47
|
+
def subnet_ids(*values)
|
48
|
+
_options[:subnet_ids] = values.join(',')
|
49
|
+
end
|
50
|
+
|
51
|
+
def as_definition
|
52
|
+
validate! if _options[:id]
|
53
|
+
|
54
|
+
{
|
55
|
+
cidr: '10.0.0.0/16',
|
56
|
+
subnet1: '10.0.0.0/24',
|
57
|
+
subnet2: '10.0.1.0/24',
|
58
|
+
subnet3: '10.0.2.0/24'
|
59
|
+
}.merge(_options)
|
60
|
+
end
|
61
|
+
|
62
|
+
def validate!
|
63
|
+
[
|
64
|
+
:subnet1, :subnet_ids, :availability_zones
|
65
|
+
].each { |key| raise "Missing required parameter #{key}" unless _options[key] }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -5,25 +5,23 @@ module EcsDeployCli
|
|
5
5
|
class Container
|
6
6
|
include AutoOptions
|
7
7
|
|
8
|
+
allowed_options :image, :cpu, :working_directory
|
9
|
+
|
8
10
|
def initialize(name, config)
|
9
11
|
@config = config
|
10
12
|
_options[:name] = name.to_s
|
11
13
|
end
|
12
14
|
|
13
|
-
def image(value)
|
14
|
-
_options[:image] = value
|
15
|
-
end
|
16
|
-
|
17
15
|
def command(*command)
|
18
16
|
_options[:command] = command
|
19
17
|
end
|
20
18
|
|
21
19
|
def load_envs(name)
|
22
|
-
_options[:environment] = (_options[:environment] || []) + YAML.safe_load(File.open(name))
|
20
|
+
_options[:environment] = (_options[:environment] || []) + YAML.safe_load(File.open(name), symbolize_names: true)
|
23
21
|
end
|
24
22
|
|
25
23
|
def env(key:, value:)
|
26
|
-
(_options[:environment] ||= []) << {
|
24
|
+
(_options[:environment] ||= []) << { name: key, value: value }
|
27
25
|
end
|
28
26
|
|
29
27
|
def secret(key:, value:)
|
@@ -34,10 +32,6 @@ module EcsDeployCli
|
|
34
32
|
(_options[:port_mappings] ||= []) << options
|
35
33
|
end
|
36
34
|
|
37
|
-
def cpu(value)
|
38
|
-
_options[:cpu] = value
|
39
|
-
end
|
40
|
-
|
41
35
|
def memory(limit:, reservation:)
|
42
36
|
_options[:memory] = limit
|
43
37
|
_options[:memory_reservation] = reservation
|
@@ -62,6 +56,10 @@ module EcsDeployCli
|
|
62
56
|
|
63
57
|
def as_definition
|
64
58
|
{
|
59
|
+
cpu: 0,
|
60
|
+
mount_points: [],
|
61
|
+
port_mappings: [],
|
62
|
+
volumes_from: [],
|
65
63
|
memory_reservation: nil,
|
66
64
|
essential: true
|
67
65
|
}.merge(_options)
|
@@ -40,8 +40,11 @@ module EcsDeployCli
|
|
40
40
|
@crons[name].instance_exec(&block)
|
41
41
|
end
|
42
42
|
|
43
|
-
def cluster(name)
|
43
|
+
def cluster(name, &block)
|
44
44
|
config[:cluster] = name
|
45
|
+
@cluster ||= {}.with_indifferent_access
|
46
|
+
@cluster = Cluster.new(name, config)
|
47
|
+
@cluster.instance_exec(&block) if block
|
45
48
|
end
|
46
49
|
|
47
50
|
def config
|
@@ -55,10 +58,11 @@ module EcsDeployCli
|
|
55
58
|
end
|
56
59
|
|
57
60
|
def resolve
|
58
|
-
resolved_containers = @containers.transform_values(&:as_definition)
|
59
|
-
resolved_tasks = @tasks.transform_values { |t| t.as_definition(resolved_containers) }
|
60
|
-
resolved_crons = @crons.transform_values { |t| t.as_definition(resolved_tasks) }
|
61
|
-
|
61
|
+
resolved_containers = (@containers || {}).transform_values(&:as_definition)
|
62
|
+
resolved_tasks = (@tasks || {}).transform_values { |t| t.as_definition(resolved_containers) }
|
63
|
+
resolved_crons = (@crons || {}).transform_values { |t| t.as_definition(resolved_tasks) }
|
64
|
+
resolved_cluster = @cluster.as_definition
|
65
|
+
[@services, resolved_tasks, resolved_crons, resolved_cluster]
|
62
66
|
end
|
63
67
|
|
64
68
|
def self.load(file)
|
@@ -18,13 +18,42 @@ module EcsDeployCli
|
|
18
18
|
_options
|
19
19
|
end
|
20
20
|
|
21
|
+
def load_balancer(name, &block)
|
22
|
+
@load_balancers ||= []
|
23
|
+
|
24
|
+
load_balancer = LoadBalancer.new(name, @config)
|
25
|
+
load_balancer.instance_exec(&block)
|
26
|
+
|
27
|
+
@load_balancers << load_balancer
|
28
|
+
end
|
29
|
+
|
21
30
|
def as_definition(task)
|
22
31
|
{
|
23
32
|
cluster: @config[:cluster],
|
24
|
-
service:
|
25
|
-
task_definition: task
|
33
|
+
service: _options[:service],
|
34
|
+
task_definition: task,
|
35
|
+
load_balancers: @load_balancers&.map(&:as_definition) || []
|
26
36
|
}
|
27
37
|
end
|
38
|
+
|
39
|
+
class LoadBalancer
|
40
|
+
include AutoOptions
|
41
|
+
allowed_options :container_name, :container_port
|
42
|
+
|
43
|
+
def initialize(name, config)
|
44
|
+
_options[:load_balancer_name] = name
|
45
|
+
@config = config
|
46
|
+
end
|
47
|
+
|
48
|
+
def target_group_arn(value)
|
49
|
+
_options[:target_group_arn] = "arn:aws:elasticloadbalancing:#{@config[:aws_region]}:#{@config[:aws_profile_id]}:targetgroup/#{value}"
|
50
|
+
_options.delete(:load_balancer_name)
|
51
|
+
end
|
52
|
+
|
53
|
+
def as_definition
|
54
|
+
_options
|
55
|
+
end
|
56
|
+
end
|
28
57
|
end
|
29
58
|
end
|
30
59
|
end
|
@@ -1,153 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ecs_deploy_cli/runners/base'
|
4
|
+
require 'ecs_deploy_cli/runners/ssh'
|
5
|
+
require 'ecs_deploy_cli/runners/validate'
|
6
|
+
require 'ecs_deploy_cli/runners/diff'
|
7
|
+
require 'ecs_deploy_cli/runners/update_crons'
|
8
|
+
require 'ecs_deploy_cli/runners/update_services'
|
9
|
+
require 'ecs_deploy_cli/runners/run_task'
|
10
|
+
require 'ecs_deploy_cli/runners/setup'
|
11
|
+
|
1
12
|
module EcsDeployCli
|
2
13
|
class Runner
|
3
14
|
def initialize(parser)
|
4
15
|
@parser = parser
|
5
16
|
end
|
6
17
|
|
18
|
+
def setup!
|
19
|
+
EcsDeployCli::Runners::Setup.new(@parser).run!
|
20
|
+
end
|
21
|
+
|
7
22
|
def validate!
|
8
|
-
@parser.
|
23
|
+
EcsDeployCli::Runners::Validate.new(@parser).run!
|
9
24
|
end
|
10
25
|
|
11
26
|
def update_crons!
|
12
|
-
|
13
|
-
|
14
|
-
crons.each do |cron_name, cron_definition|
|
15
|
-
task_definition = tasks[cron_definition[:task_name]]
|
16
|
-
raise "Undefined task #{cron_definition[:task_name].inspect} in (#{tasks.keys.inspect})" unless task_definition
|
17
|
-
|
18
|
-
updated_task = _update_task(task_definition)
|
19
|
-
|
20
|
-
current_target = cwe_client.list_targets_by_rule(
|
21
|
-
{
|
22
|
-
rule: cron_name,
|
23
|
-
limit: 1
|
24
|
-
}
|
25
|
-
).to_h[:targets].first
|
26
|
-
|
27
|
-
cwe_client.put_rule(
|
28
|
-
cron_definition[:rule]
|
29
|
-
)
|
30
|
-
|
31
|
-
cwe_client.put_targets(
|
32
|
-
rule: cron_name,
|
33
|
-
targets: [
|
34
|
-
id: current_target[:id],
|
35
|
-
arn: current_target[:arn],
|
36
|
-
role_arn: current_target[:role_arn],
|
37
|
-
input: cron_definition[:input].to_json,
|
38
|
-
ecs_parameters: cron_definition[:ecs_parameters].merge(task_definition_arn: updated_task[:task_definition_arn])
|
39
|
-
]
|
40
|
-
)
|
41
|
-
EcsDeployCli.logger.info "Deployed scheduled task \"#{cron_name}\"!"
|
42
|
-
end
|
27
|
+
EcsDeployCli::Runners::UpdateCrons.new(@parser).run!
|
43
28
|
end
|
44
29
|
|
45
|
-
def
|
46
|
-
|
47
|
-
|
48
|
-
).to_h[:container_instance_arns]
|
49
|
-
|
50
|
-
response = ecs_client.describe_container_instances(
|
51
|
-
cluster: config[:cluster],
|
52
|
-
container_instances: instances
|
53
|
-
)
|
54
|
-
|
55
|
-
EcsDeployCli.logger.info "Found instances: #{response.container_instances.map(&:ec2_instance_id).join(', ')}"
|
56
|
-
|
57
|
-
response = ec2_client.describe_instances(
|
58
|
-
instance_ids: response.container_instances.map(&:ec2_instance_id)
|
59
|
-
)
|
30
|
+
def run_task!(task_name, launch_type:, security_groups:, subnets:)
|
31
|
+
EcsDeployCli::Runners::RunTask.new(@parser).run!(task_name, launch_type: launch_type, security_groups: security_groups, subnets: subnets)
|
32
|
+
end
|
60
33
|
|
61
|
-
|
62
|
-
EcsDeployCli.
|
34
|
+
def ssh(**options)
|
35
|
+
EcsDeployCli::Runners::SSH.new(@parser).run!(options)
|
36
|
+
end
|
63
37
|
|
64
|
-
|
65
|
-
|
38
|
+
def diff
|
39
|
+
EcsDeployCli::Runners::Diff.new(@parser).run!
|
66
40
|
end
|
67
41
|
|
68
42
|
def update_services!(service: nil, timeout: 500)
|
69
|
-
|
70
|
-
|
71
|
-
services.each do |service_name, service_definition|
|
72
|
-
next if !service.nil? && service != service_name
|
73
|
-
|
74
|
-
task_definition = _update_task resolved_tasks[service_definition.options[:task]]
|
75
|
-
task_name = "#{task_definition[:family]}:#{task_definition[:revision]}"
|
76
|
-
|
77
|
-
ecs_client.update_service(
|
78
|
-
cluster: config[:cluster],
|
79
|
-
service: service_name,
|
80
|
-
task_definition: "#{task_definition[:family]}:#{task_name}"
|
81
|
-
)
|
82
|
-
wait_for_deploy(service_name, task_name, timeout: timeout)
|
83
|
-
EcsDeployCli.logger.info "Deployed service \"#{service_name}\"!"
|
84
|
-
end
|
43
|
+
EcsDeployCli::Runners::UpdateServices.new(@parser).run!(service: service, timeout: timeout)
|
85
44
|
end
|
86
45
|
|
87
46
|
private
|
88
47
|
|
89
|
-
def wait_for_deploy(service_name, task_name, timeout:)
|
90
|
-
wait_data = { cluster: config[:cluster], services: [service_name] }
|
91
|
-
|
92
|
-
started_at = Time.now
|
93
|
-
ecs_client.wait_until(
|
94
|
-
:services_stable, wait_data,
|
95
|
-
max_attempts: nil,
|
96
|
-
before_wait: lambda { |_, response|
|
97
|
-
deployments = response.services.first.deployments
|
98
|
-
log_deployments task_name, deployments
|
99
|
-
|
100
|
-
throw :success if deployments.count == 1 && deployments[0].task_definition.end_with?(task_name)
|
101
|
-
throw :failure if Time.now - started_at > timeout
|
102
|
-
}
|
103
|
-
)
|
104
|
-
end
|
105
|
-
|
106
48
|
def _update_task(definition)
|
107
49
|
ecs_client.register_task_definition(
|
108
50
|
definition
|
109
51
|
).to_h[:task_definition]
|
110
52
|
end
|
111
|
-
|
112
|
-
def log_deployments(task_name, deployments)
|
113
|
-
EcsDeployCli.logger.info "Waiting for task: #{task_name} to become ok."
|
114
|
-
EcsDeployCli.logger.info 'Deployment status:'
|
115
|
-
deployments.each do |deploy|
|
116
|
-
EcsDeployCli.logger.info "[#{deploy.status}] task=#{deploy.task_definition.split('/').last}, "\
|
117
|
-
"desired_count=#{deploy.desired_count}, pending_count=#{deploy.pending_count}, running_count=#{deploy.running_count}, failed_tasks=#{deploy.failed_tasks}"
|
118
|
-
end
|
119
|
-
EcsDeployCli.logger.info ''
|
120
|
-
end
|
121
|
-
|
122
|
-
def ec2_client
|
123
|
-
@ec2_client ||= begin
|
124
|
-
require 'aws-sdk-ec2'
|
125
|
-
Aws::EC2::Client.new(
|
126
|
-
profile: ENV.fetch('AWS_PROFILE', 'default'),
|
127
|
-
region: config[:aws_region]
|
128
|
-
)
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
def ecs_client
|
133
|
-
@ecs_client ||= Aws::ECS::Client.new(
|
134
|
-
profile: ENV.fetch('AWS_PROFILE', 'default'),
|
135
|
-
region: config[:aws_region]
|
136
|
-
)
|
137
|
-
end
|
138
|
-
|
139
|
-
def cwe_client
|
140
|
-
@cwe_client ||= begin
|
141
|
-
require 'aws-sdk-cloudwatchevents'
|
142
|
-
Aws::CloudWatchEvents::Client.new(
|
143
|
-
profile: ENV.fetch('AWS_PROFILE', 'default'),
|
144
|
-
region: config[:aws_region]
|
145
|
-
)
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
def config
|
150
|
-
@parser.config
|
151
|
-
end
|
152
53
|
end
|
153
54
|
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EcsDeployCli
|
4
|
+
module Runners
|
5
|
+
class Base
|
6
|
+
def initialize(parser)
|
7
|
+
@parser = parser
|
8
|
+
end
|
9
|
+
|
10
|
+
def run!
|
11
|
+
raise NotImplementedError, 'abstract method'
|
12
|
+
end
|
13
|
+
|
14
|
+
def update_task(definition)
|
15
|
+
_update_task(definition)
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def _update_task(definition)
|
21
|
+
definition[:container_definitions].each do |container|
|
22
|
+
next unless container.dig(:log_configuration, :log_driver) == 'awslogs'
|
23
|
+
|
24
|
+
_create_cloudwatch_logs_if_needed(container.dig(:log_configuration, :options, 'awslogs-group'))
|
25
|
+
end
|
26
|
+
|
27
|
+
ecs_client.register_task_definition(
|
28
|
+
definition
|
29
|
+
).to_h[:task_definition]
|
30
|
+
end
|
31
|
+
|
32
|
+
def _create_cloudwatch_logs_if_needed(prefix)
|
33
|
+
log_group = cwl_client.describe_log_groups(log_group_name_prefix: prefix, limit: 1).to_h[:log_groups]
|
34
|
+
return if log_group.any?
|
35
|
+
|
36
|
+
cwl_client.create_log_group(log_group_name: prefix)
|
37
|
+
cwl_client.put_retention_policy(
|
38
|
+
log_group_name: prefix,
|
39
|
+
retention_in_days: 14
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def ec2_client
|
44
|
+
@ec2_client ||= begin
|
45
|
+
require 'aws-sdk-ec2'
|
46
|
+
Aws::EC2::Client.new(
|
47
|
+
profile: ENV.fetch('AWS_PROFILE', 'default'),
|
48
|
+
region: config[:aws_region]
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def ecs_client
|
54
|
+
@ecs_client ||= Aws::ECS::Client.new(
|
55
|
+
profile: ENV.fetch('AWS_PROFILE', 'default'),
|
56
|
+
region: config[:aws_region]
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
def cwe_client
|
61
|
+
@cwe_client ||= begin
|
62
|
+
require 'aws-sdk-cloudwatchevents'
|
63
|
+
Aws::CloudWatchEvents::Client.new(
|
64
|
+
profile: ENV.fetch('AWS_PROFILE', 'default'),
|
65
|
+
region: config[:aws_region]
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def ssm_client
|
71
|
+
@cwl_client ||= begin
|
72
|
+
require 'aws-sdk-ssm'
|
73
|
+
Aws::SSM::Client.new(
|
74
|
+
profile: ENV.fetch('AWS_PROFILE', 'default'),
|
75
|
+
region: config[:aws_region]
|
76
|
+
)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def cwl_client
|
81
|
+
@cwl_client ||= begin
|
82
|
+
require 'aws-sdk-cloudwatchlogs'
|
83
|
+
Aws::CloudWatchLogs::Client.new(
|
84
|
+
profile: ENV.fetch('AWS_PROFILE', 'default'),
|
85
|
+
region: config[:aws_region]
|
86
|
+
)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def cf_client
|
91
|
+
@cl_client ||= begin
|
92
|
+
require 'aws-sdk-cloudformation'
|
93
|
+
Aws::CloudFormation::Client.new(
|
94
|
+
profile: ENV.fetch('AWS_PROFILE', 'default'),
|
95
|
+
region: config[:aws_region]
|
96
|
+
)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def config
|
101
|
+
@parser.config
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|