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

Commit 59d50bc

Browse files
committed
🆕 Improve README, scheduled tasks implementation, more options
1 parent 704ff79 commit 59d50bc

File tree

14 files changed

+415
-71
lines changed

14 files changed

+415
-71
lines changed

README.md

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,37 @@
44

55
A CLI + DSL to simplify deployments on ECS.
66

7-
It's partial, incomplete and unstable, DON'T use yet.
7+
It's partial, incomplete and unstable. Use it at your own risk.
88

9-
## Installation
9+
## Motivation
1010

11-
Simply add the gem to your Gemfile
11+
Once you've configured your cluster on ECS, running Continous Deployment is not that easy.
1212

13-
```ruby
14-
gem 'ecs-deploy-cli', github: 'monade/ecs-deploy-cli'
15-
```
13+
A simple deployment requires:
14+
* Upload the Docker image
15+
* Update all your tasks
16+
* Update all services
17+
* Update eventual Scheduled Tasks manually
18+
19+
We had some struggle with the official `ecs-cli` approach related with static compose files, requiring a lot of repetition (and potential errors) while defining tasks and envs.
20+
21+
Moreover, you might want some business logic about how your cluster should be configured, like using ENV files, or switch between stages (production / staging), or adjusting container requirements based on external variables.
22+
23+
So, why not creating a DSL built on top of our favourite language? <3
24+
25+
## Installation
1626

17-
Or install it globally to use it as a cli:
27+
You can install this gem globally
1828
```bash
1929
$ gem install ecs-deploy-cli
2030
```
2131

32+
Or add the gem to your Gemfile:
33+
34+
```ruby
35+
gem 'ecs-deploy-cli'
36+
```
37+
2238
## Usage
2339

2440
First, define a ECSFile in your project, to design your ECS cluster.
@@ -40,6 +56,7 @@ container :base_container do
4056
image "#{ENV['REPO_URL']}:#{ENV['CURRENT_VERSION']}"
4157
load_envs 'envs/base.yml'
4258
load_envs 'envs/production.yml'
59+
env key: 'MANUAL_ENV', value: '123'
4360
secret key: 'RAILS_MASTER_KEY', value: 'railsMasterKey' # Taking the secret from AWS System Manager with name "arn:aws:ssm:__AWS_REGION__:__AWS_PROFILE_ID__:parameter/railsMasterKey"
4461
working_directory '/app'
4562
cloudwatch_logs 'yourproject' # Configuring cloudwatch logs
@@ -96,24 +113,60 @@ end
96113
# Scheduled tasks using Cloudwatch Events / Eventbridge
97114
cron :scheduled_emails do
98115
task :'yourproject-cron' do
99-
# Overrides
100-
container :web do
116+
# Container overrides
117+
container :cron do
101118
command 'rails', 'cron:exec'
102119
end
103120
end
121+
subnets 'subnet-123123'
122+
launch_type 'FARGATE'
123+
# Task role override:
124+
# task_role 'somerole'
125+
104126
# Examples:
105-
# every 1.hour
127+
# run_every '2 hours'
106128
run_at '0 * * * ? *'
107129
end
108130
```
109131

110132
Now you can run the commands from the CLI.
111133

112-
For instance, you can deploy all services:
134+
For instance, you can run deploy:
135+
```bash
136+
$ ecs-deploy deploy
137+
```
138+
139+
## CLI commands
140+
141+
You can find the full command list by running `ecs-deploy help`.
142+
143+
Check if your ECSFile is valid
144+
```bash
145+
$ ecs-deploy validate
146+
```
147+
148+
Deploy all services and scheduled tasks
149+
```bash
150+
$ ecs-deploy deploy
151+
```
152+
153+
Deploy just services
113154
```bash
114155
$ ecs-deploy deploy-services
115156
```
116157

158+
Deploy just scheduled tasks
159+
```bash
160+
$ ecs-deploy deploy-services
161+
```
162+
163+
Run SSH on a cluster container instance:
164+
```bash
165+
$ ecs-deploy ssh
166+
```
167+
168+
## API
169+
117170
You can also use it as an API:
118171
```ruby
119172
require 'ecs_deploy_cli'
@@ -124,9 +177,9 @@ runner = EcsDeployCli::Runner.new(parser)
124177
runner.update_services!
125178
```
126179

127-
### TODOs
180+
## TODOs
128181

129-
- Scheduled tasks implementation
130-
- SSH to ec2 instances
182+
- Create cloudwatch logs group if it doesn't exist yet
183+
- Create the service if not present?
184+
- Create scheduled tasks if not present?
131185
- More configuration options
132-
- Create the service if not present

ecs-deploy-cli.gemspec

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ Gem::Specification.new do |s|
77
s.name = 'ecs_deploy_cli'
88
s.version = EcsDeployCli::VERSION
99
s.date = '2021-03-31'
10-
s.summary = "A command line interface to make ECS deployments easier"
11-
s.description = "Declare your cluster structure in a ECSFile and use the CLI to run deploys and monitor its status."
10+
s.summary = 'A command line interface to make ECS deployments easier'
11+
s.description = 'Declare your cluster structure in a ECSFile and use the CLI to run deploys and monitor its status.'
1212
s.authors = ['Mònade', 'ProGM']
1313
s.email = 'team@monade.io'
1414
s.files = Dir['lib/**/*']
@@ -18,8 +18,10 @@ Gem::Specification.new do |s|
1818
s.license = 'MIT'
1919
s.executables << 'ecs-deploy'
2020
s.add_dependency 'activesupport', ['>= 5', '< 7']
21-
s.add_dependency 'thor'
22-
s.add_dependency 'aws-sdk-ecs'
21+
s.add_dependency 'aws-sdk-cloudwatchevents', '~> 1'
22+
s.add_dependency 'aws-sdk-ec2', '~> 1'
23+
s.add_dependency 'aws-sdk-ecs', '~> 1'
24+
s.add_dependency 'thor', '~> 1.1'
2325
s.add_development_dependency 'rspec', '~> 3'
24-
s.add_development_dependency 'rubocop'
26+
s.add_development_dependency 'rubocop', '~> 0.93'
2527
end

lib/ecs_deploy_cli.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require 'logger'
33
require 'thor'
44
require 'aws-sdk-ecs'
5+
require 'active_support/core_ext/hash/indifferent_access'
56

67
module EcsDeployCli
78
def self.logger

lib/ecs_deploy_cli/cli.rb

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,44 @@
11
module EcsDeployCli
22
class CLI < Thor
3-
desc "validate", "Validates your ECSFile"
3+
desc 'validate', 'Validates your ECSFile'
44
option :file, default: 'ECSFile'
55
def validate
66
@parser = load(options[:file])
7-
@parser.validate!
8-
puts "Your ECSFile looks fine! 🎉"
7+
runner.validate!
8+
puts 'Your ECSFile looks fine! 🎉'
99
end
1010

11-
desc "version", "Updates all services defined in your ECSFile"
11+
desc 'version', 'Updates all services defined in your ECSFile'
1212
def version
1313
puts "ECS Deploy CLI Version #{EcsDeployCli::VERSION}."
1414
end
1515

16-
desc "deploy-services", "Updates all services defined in your ECSFile"
16+
desc 'deploy-scheduled-tasks', 'Updates all scheduled tasks defined in your ECSFile'
1717
option :file, default: 'ECSFile'
18-
option :timeout, type: :numeric, default: 500
19-
def deploy_services
18+
def deploy_scheduled_tasks
2019
@parser = load(options[:file])
21-
runner.update_services! timeout: options[:timeout]
20+
runner.update_crons!
2221
end
2322

24-
desc "deploy-service", "Updates a single service defined in your ECSFile"
23+
desc 'deploy-services', 'Updates all services defined in your ECSFile'
24+
option :only
2525
option :file, default: 'ECSFile'
2626
option :timeout, type: :numeric, default: 500
27-
def deploy_service(name)
27+
def deploy_services
2828
@parser = load(options[:file])
29-
runner.update_services! service: name, timeout: options[:timeout]
29+
runner.update_services! timeout: options[:timeout], service: options[:only]
3030
end
3131

32-
desc "deploy-scheduled-tasks", "Updates all scheduled tasks defined in your ECSFile"
32+
desc 'deploy', 'Updates a single service defined in your ECSFile'
3333
option :file, default: 'ECSFile'
34-
def deploy_scheduled_tasks
34+
option :timeout, type: :numeric, default: 500
35+
def deploy
3536
@parser = load(options[:file])
37+
runner.update_services! timeout: options[:timeout]
3638
runner.update_crons!
3739
end
3840

39-
desc "ssh", "Connects to ECS instance via SSH"
41+
desc 'ssh', 'Connects to ECS instance via SSH'
4042
option :file, default: 'ECSFile'
4143
def ssh
4244
@parser = load(options[:file])

lib/ecs_deploy_cli/dsl/auto_options.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module DSL
33
module AutoOptions
44
def method_missing(name, *args, &block)
55
if args.count == 1 && !block
6+
EcsDeployCli.logger.info("Auto-added option security_group #{name.to_sym} = #{args.first}")
67
_options[name.to_sym] = args.first
78
else
89
super

lib/ecs_deploy_cli/dsl/container.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ def initialize(name, config)
1010
_options[:name] = name.to_s
1111
end
1212

13+
def image(value)
14+
_options[:image] = value
15+
end
16+
1317
def command(*command)
1418
_options[:command] = command
1519
end
@@ -18,6 +22,10 @@ def load_envs(name)
1822
_options[:environment] = (_options[:environment] || []) + YAML.safe_load(File.open(name))
1923
end
2024

25+
def env(key:, value:)
26+
(_options[:environment] ||= []) << { 'name' => key, 'value' => value }
27+
end
28+
2129
def secret(key:, value:)
2230
(_options[:secrets] ||= []) << { name: key, value_from: "arn:aws:ssm:#{@config[:aws_region]}:#{@config[:aws_profile_id]}:parameter/#{value}" }
2331
end
@@ -26,6 +34,10 @@ def expose(**options)
2634
(_options[:port_mappings] ||= []) << options
2735
end
2836

37+
def cpu(value)
38+
_options[:cpu] = value
39+
end
40+
2941
def memory(limit:, reservation:)
3042
_options[:memory] = limit
3143
_options[:memory_reservation] = reservation

lib/ecs_deploy_cli/dsl/cron.rb

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,99 @@ module DSL
55
class Cron
66
include AutoOptions
77

8-
def initialize(name, config); end
8+
def initialize(name, config)
9+
_options[:name] = name
10+
@config = config
11+
end
12+
13+
def task(name, &block)
14+
_options[:task] = Task.new(name.to_s, @config)
15+
_options[:task].instance_exec(&block)
16+
end
17+
18+
def run_at(cron_expression)
19+
@cron_expression = "cron(#{cron_expression})"
20+
end
21+
22+
def run_every(interval)
23+
@every = "rate(#{interval})"
24+
end
25+
26+
def task_role(role)
27+
_options[:task_role] = "arn:aws:iam::#{@config[:aws_profile_id]}:role/#{role}"
28+
end
29+
30+
def subnets(*value)
31+
_options[:subnets] = value
32+
end
33+
34+
def security_groups(*value)
35+
_options[:security_groups] = value
36+
end
37+
38+
def launch_type(value)
39+
_options[:launch_type] = value
40+
end
41+
42+
def assign_public_ip(value)
43+
_options[:assign_public_ip] = value
44+
end
45+
46+
def as_definition(tasks)
47+
raise 'Missing task definition' unless _options[:task]
48+
49+
input = { 'containerOverrides' => _options[:task].as_definition }
50+
input['taskRoleArn'] = _options[:task_role] if _options[:task_role]
51+
52+
{
53+
task_name: _options[:task].name,
54+
rule: {
55+
name: _options[:name],
56+
schedule_expression: @cron_expression || @every || raise("Missing schedule expression.")
57+
},
58+
input: input,
59+
ecs_parameters: {
60+
# task_definition_arn: task_definition[:task_definition_arn],
61+
task_count: _options[:task_count] || 1,
62+
launch_type: _options[:launch_type] || raise('Missing parameter launch_type'),
63+
network_configuration: {
64+
awsvpc_configuration: {
65+
subnets: _options[:subnets] || raise('Missing parameter subnets'),
66+
security_groups: _options[:security_groups] || [],
67+
assign_public_ip: _options[:assign_public_ip] ? 'ENABLED' : 'DISABLED'
68+
}
69+
},
70+
platform_version: _options[:platform_version] || 'LATEST'
71+
}
72+
}
73+
end
74+
75+
class Task
76+
include AutoOptions
77+
78+
attr_reader :name
79+
80+
def initialize(name, config)
81+
@name = name
82+
@config = config
83+
end
84+
85+
def container(name, &block)
86+
container = Container.new(name, @config)
87+
container.instance_exec(&block)
88+
(_options[:containers] ||= []) << container
89+
end
990

10-
def task(name)
11-
_options[:task] = name.to_s
91+
def as_definition
92+
# [{"name"=>"cron", "command"=>["rails", "cron:adalytics"]}]
93+
(_options[:containers] || []).map(&:as_definition)
94+
end
1295
end
1396

14-
def as_definition(containers)
15-
raise NotImplementedError
97+
class Container < EcsDeployCli::DSL::Container
98+
def as_definition
99+
_options.to_h
100+
end
16101
end
17102
end
18103
end

0 commit comments

Comments
 (0)