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
@@ -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
- EcsDeployCli.logger.info("Auto-added option security_group #{name.to_sym} = #{args.first}")
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] ||= []) << { 'name' => key, 'value' => value }
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
- [@services, resolved_tasks, resolved_crons]
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: service_name,
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
@@ -5,6 +5,8 @@ module EcsDeployCli
5
5
  class Task
6
6
  include AutoOptions
7
7
 
8
+ allowed_options :requires_compatibilities, :network_mode
9
+
8
10
  def initialize(name, config)
9
11
  @config = config
10
12
  _options[:family] = name.to_s
@@ -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.resolve
23
+ EcsDeployCli::Runners::Validate.new(@parser).run!
9
24
  end
10
25
 
11
26
  def update_crons!
12
- _, tasks, crons = @parser.resolve
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 ssh
46
- instances = ecs_client.list_container_instances(
47
- cluster: config[:cluster]
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
- dns_name = response.reservations[0].instances[0].public_dns_name
62
- EcsDeployCli.logger.info "Connecting to ec2-user@#{dns_name}..."
34
+ def ssh(**options)
35
+ EcsDeployCli::Runners::SSH.new(@parser).run!(options)
36
+ end
63
37
 
64
- Process.fork { exec("ssh ec2-user@#{dns_name}") }
65
- Process.wait
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
- services, resolved_tasks = @parser.resolve
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