Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 391a8b1

Browse files
committed
🆕 New version 0.2.0
1 parent e82146f commit 391a8b1

23 files changed

+566
-166
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
# Changelog
22
All notable changes to this project made by Monade Team are documented in this file. For info refer to team@monade.io
33

4-
## [0.1.0-alpha] - 2021-03-21
4+
## [0.2.0] - 2021-03-25
5+
### Added
6+
- Command run-task
7+
- Command diff
8+
- Made automatic options explicit
9+
- Deploy scheduled tasks
10+
- Refactoring runners
11+
12+
## [0.1.0] - 2021-03-21
513
First functional version
614

715
### Added

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# ECS Deploy CLI
44

5-
A CLI + DSL to simplify deployments on ECS.
5+
A CLI + DSL to simplify deployments on AWS [Elastic Container Service](https://aws.amazon.com/it/ecs/).
66

77
It's partial, incomplete and unstable. Use it at your own risk.
88

@@ -160,6 +160,16 @@ Deploy just scheduled tasks
160160
$ ecs-deploy deploy-scheduled-tasks
161161
```
162162

163+
Prints the diff between your local task_definitions and the ones in your AWS account. Useful to debug what has to be updated using `deploy`.
164+
```bash
165+
$ ecs-deploy diff
166+
```
167+
168+
Starts a task in the cluster based on a task definition.
169+
```bash
170+
$ ecs-deploy run-task [task_name] --subnets subnet1,subnet2 --launch-type FARGATE|EC2 --security-groups sg-123,sg-234
171+
```
172+
163173
Run SSH on a cluster container instance:
164174
```bash
165175
$ ecs-deploy ssh

ecs-deploy-cli.gemspec

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@ Gem::Specification.new do |s|
1515
s.test_files = Dir['spec/**/*']
1616
s.required_ruby_version = '>= 2.5.0'
1717
s.homepage = 'https://rubygems.org/gems/ecs_deploy_cli'
18+
s.metadata = { 'source_code_uri' => 'https://github.com/monade/ecs-deploy-cli' }
1819
s.license = 'MIT'
1920
s.executables << 'ecs-deploy'
2021
s.add_dependency 'activesupport', ['>= 5', '< 7']
2122
s.add_dependency 'aws-sdk-cloudwatchevents', '~> 1'
2223
s.add_dependency 'aws-sdk-ec2', '~> 1'
2324
s.add_dependency 'aws-sdk-ecs', '~> 1'
25+
s.add_dependency 'colorize', '~> 0.8.1'
26+
s.add_dependency 'hashdiff', '~> 1.0'
2427
s.add_dependency 'thor', '~> 1.1'
2528
s.add_development_dependency 'rspec', '~> 3'
2629
s.add_development_dependency 'rubocop', '~> 0.93'

lib/ecs_deploy_cli.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
# frozen_string_literal: true
2+
13
require 'yaml'
24
require 'logger'
35
require 'thor'
46
require 'aws-sdk-ecs'
57
require 'active_support/core_ext/hash/indifferent_access'
8+
require 'active_support/concern'
69

710
module EcsDeployCli
811
def self.logger
912
@logger ||= begin
1013
logger = Logger.new(STDOUT)
11-
logger.formatter = proc { |severity, datetime, progname, msg|
14+
logger.formatter = proc { |_severity, _datetime, _progname, msg|
1215
"#{msg}\n"
1316
}
1417
logger.level = Logger::INFO

lib/ecs_deploy_cli/cli.rb

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
# frozen_string_literal: true
2+
13
module EcsDeployCli
24
class CLI < Thor
5+
def self.exit_on_failure?
6+
true
7+
end
8+
39
desc 'validate', 'Validates your ECSFile'
410
option :file, default: 'ECSFile'
511
def validate
@@ -8,6 +14,13 @@ def validate
814
puts 'Your ECSFile looks fine! 🎉'
915
end
1016

17+
desc 'diff', 'Check differences between task definitions'
18+
option :file, default: 'ECSFile'
19+
def diff
20+
@parser = load(options[:file])
21+
runner.diff
22+
end
23+
1124
desc 'version', 'Updates all services defined in your ECSFile'
1225
def version
1326
puts "ECS Deploy CLI Version #{EcsDeployCli::VERSION}."
@@ -29,7 +42,7 @@ def deploy_services
2942
runner.update_services! timeout: options[:timeout], service: options[:only]
3043
end
3144

32-
desc 'deploy', 'Updates a single service defined in your ECSFile'
45+
desc 'deploy', 'Updates all services and scheduled tasks at once'
3346
option :file, default: 'ECSFile'
3447
option :timeout, type: :numeric, default: 500
3548
def deploy
@@ -38,6 +51,21 @@ def deploy
3851
runner.update_crons!
3952
end
4053

54+
desc 'run-task NAME', 'Manually runs a task defined in your ECSFile'
55+
option :launch_type, default: 'FARGATE'
56+
option :security_groups, default: '', type: :string
57+
option :subnets, required: true, type: :string
58+
option :file, default: 'ECSFile'
59+
def run_task(task_name)
60+
@parser = load(options[:file])
61+
runner.run_task!(
62+
task_name,
63+
launch_type: options[:launch_type],
64+
security_groups: options[:security_groups].split(','),
65+
subnets: options[:subnets].split(',')
66+
)
67+
end
68+
4169
desc 'ssh', 'Connects to ECS instance via SSH'
4270
option :file, default: 'ECSFile'
4371
def ssh

lib/ecs_deploy_cli/dsl/auto_options.rb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
1+
# frozen_string_literal: true
2+
13
module EcsDeployCli
24
module DSL
35
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+
418
def method_missing(name, *args, &block)
519
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
723
_options[name.to_sym] = args.first
824
else
925
super

lib/ecs_deploy_cli/dsl/container.rb

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,23 @@ module DSL
55
class Container
66
include AutoOptions
77

8+
allowed_options :image, :cpu, :working_directory
9+
810
def initialize(name, config)
911
@config = config
1012
_options[:name] = name.to_s
1113
end
1214

13-
def image(value)
14-
_options[:image] = value
15-
end
16-
1715
def command(*command)
1816
_options[:command] = command
1917
end
2018

2119
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)
2321
end
2422

2523
def env(key:, value:)
26-
(_options[:environment] ||= []) << { 'name' => key, 'value' => value }
24+
(_options[:environment] ||= []) << { name: key, value: value }
2725
end
2826

2927
def secret(key:, value:)
@@ -34,10 +32,6 @@ def expose(**options)
3432
(_options[:port_mappings] ||= []) << options
3533
end
3634

37-
def cpu(value)
38-
_options[:cpu] = value
39-
end
40-
4135
def memory(limit:, reservation:)
4236
_options[:memory] = limit
4337
_options[:memory_reservation] = reservation

lib/ecs_deploy_cli/dsl/parser.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ def ensure_required_params!
5555
end
5656

5757
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) }
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) }
6161
[@services, resolved_tasks, resolved_crons]
6262
end
6363

lib/ecs_deploy_cli/dsl/task.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ module DSL
55
class Task
66
include AutoOptions
77

8+
allowed_options :requires_compatibilities, :network_mode
9+
810
def initialize(name, config)
911
@config = config
1012
_options[:family] = name.to_s

lib/ecs_deploy_cli/runner.rb

Lines changed: 20 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,153 +1,49 @@
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+
111
module EcsDeployCli
212
class Runner
313
def initialize(parser)
414
@parser = parser
515
end
616

717
def validate!
8-
@parser.resolve
18+
EcsDeployCli::Runners::Validate.new(@parser).run!
919
end
1020

1121
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-
)
22+
EcsDeployCli::Runners::UpdateCrons.new(@parser).run!
23+
end
3024

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
25+
def run_task!(task_name, launch_type:, security_groups:, subnets:)
26+
EcsDeployCli::Runners::RunTask.new(@parser).run!(task_name, launch_type: launch_type, security_groups: security_groups, subnets: subnets)
4327
end
4428

4529
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-
)
60-
61-
dns_name = response.reservations[0].instances[0].public_dns_name
62-
EcsDeployCli.logger.info "Connecting to ec2-user@#{dns_name}..."
30+
EcsDeployCli::Runners::SSH.new(@parser).run!
31+
end
6332

64-
Process.fork { exec("ssh ec2-user@#{dns_name}") }
65-
Process.wait
33+
def diff
34+
EcsDeployCli::Runners::Diff.new(@parser).run!
6635
end
6736

6837
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
38+
EcsDeployCli::Runners::UpdateServices.new(@parser).run!(service: service, timeout: timeout)
8539
end
8640

8741
private
8842

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-
10643
def _update_task(definition)
10744
ecs_client.register_task_definition(
10845
definition
10946
).to_h[:task_definition]
11047
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
15248
end
15349
end

0 commit comments

Comments
 (0)