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.
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