Skip to content

Commit 232cc41

Browse files
add ecs_run.rb
1 parent 60edec3 commit 232cc41

File tree

5 files changed

+211
-7
lines changed

5 files changed

+211
-7
lines changed

Gemfile

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
source 'https://rubygems.org'
22

33
gem 'aws-sdk-ssm'
4+
gem 'aws-sdk-ecs'
5+
gem 'aws-sdk-cloudwatchlogs'

Gemfile.lock

+8
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@ GEM
33
specs:
44
aws-eventstream (1.1.0)
55
aws-partitions (1.344.0)
6+
aws-sdk-cloudwatchlogs (1.34.0)
7+
aws-sdk-core (~> 3, >= 3.99.0)
8+
aws-sigv4 (~> 1.1)
69
aws-sdk-core (3.104.2)
710
aws-eventstream (~> 1, >= 1.0.2)
811
aws-partitions (~> 1, >= 1.239.0)
912
aws-sigv4 (~> 1.1)
1013
jmespath (~> 1.0)
14+
aws-sdk-ecs (1.67.0)
15+
aws-sdk-core (~> 3, >= 3.99.0)
16+
aws-sigv4 (~> 1.1)
1117
aws-sdk-ssm (1.84.0)
1218
aws-sdk-core (~> 3, >= 3.99.0)
1319
aws-sigv4 (~> 1.1)
@@ -19,6 +25,8 @@ PLATFORMS
1925
ruby
2026

2127
DEPENDENCIES
28+
aws-sdk-cloudwatchlogs
29+
aws-sdk-ecs
2230
aws-sdk-ssm
2331

2432
BUNDLED WITH

README.md

+70-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
# ssm-param-tool
1+
# aws-ecs-tools
2+
3+
A collection of Ruby scripts that make it easier to work with ECS from the command line.
4+
5+
## param_tool.rb
26

37
A tool to sync up AWS Systems Manager Parameter Store with a local YAML file.
48

@@ -11,19 +15,19 @@ Usage: param_tool.rb [options] (down|up)
1115
-d, --dry-run Do not apply changes
1216
```
1317

14-
## Download params
18+
### Download params
1519

16-
```
20+
```sh
1721
param_tool.rb --prefix /staging/myapp down >params.yml
1822
```
1923

2024
- secure (encrypted) param values are replaced with `SECURE` - NOT decrypted.
2125
- secure param keys are suffixed with '!'
2226
- param tree is unwrapped into a hash
2327

24-
## Upload params
28+
### Upload params
2529

26-
```
30+
```sh
2731
param_tool.rb --prefix /staging/myapp up <params.yml
2832

2933
# specify a key to do the encryption:
@@ -40,15 +44,15 @@ param_tool.rb --dry-run --prefix /staging/myapp up <params.yml
4044
- to make a param secure, add a `!` suffix to the key name - note that the '!' character itself will be stripped from the key name in Parameter Store
4145
- params with a value of `DELETE` are deleted from parameter store
4246

43-
## Workflow concept
47+
### Workflow concept
4448

4549
- create a YAML file with the params you need; you can reuse the same file for a file-based Global backend.
4650
- upload it to staging
4751
- upload it to prod
4852
- download params from staging, update, and send to prod
4953
- commit param set as reference (make sure that sensitive params are secured, and thus not committed)
5054

51-
## Sample params.yml
55+
### Sample params.yml
5256

5357
```yaml
5458
---
@@ -66,3 +70,62 @@ heroku:
6670
"useful": "for SSH keys"
6771
}
6872
```
73+
74+
## ecs_run.rb
75+
76+
Run a script on an ECS service
77+
78+
```sh
79+
Usage: ecs_run.rb [options] [command or STDIN]
80+
-c, --cluster=CLUSTER Cluster name
81+
-s, --service=SERVICE Service name
82+
-w, --watch Watch output
83+
-r, --ruby Run input as Ruby code with Rails runner (instead of shell command)
84+
```
85+
86+
### Specify target
87+
88+
Cluster and service are required params. Besides them, you'll need to set the region through environment variables.
89+
90+
### Providing input
91+
92+
There are three ways to provide input:
93+
94+
- as a final argument to the command - make sure to quote it properly
95+
96+
```sh
97+
ecs_run.rb -c app -s app 'rake -T'
98+
```
99+
100+
- from a file
101+
102+
```sh
103+
ecs_run.rb -c app -s app <script.sh
104+
```
105+
106+
- type it in
107+
108+
```sh
109+
ecs_run.rb -c app -s app
110+
Type your command then press Ctrl+D
111+
rake -T
112+
[Ctrl+D]
113+
```
114+
115+
Note that in all cases you're providing literal code to be evaluated on the ECS service; you can't send files; the rest of the environment is defined by the service.
116+
117+
### Watching output
118+
119+
Normally after you start the task you get an AWS Console link to monitor the task online, and that's it.
120+
121+
But if you specify the `--watch` option, you will see the task status changes and the output logged to the terminal. You will also know when the task has finished.
122+
123+
### Running Ruby code
124+
125+
Besides running shell code, you can also run Ruby code with the Rails runner (only available if `bundle` and a Rails app are present in your service's docker image.)
126+
127+
```sh
128+
ecs_run.rb -c app -s app --ruby 'p User.first'
129+
```
130+
131+
This way you get Rails log output, but note that, unlike a Rails console, you don't see command evaluation results by default - you need to print it explicitly.

ecs_run.rb

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/bin/env ruby
2+
3+
require 'aws-sdk-ecs'
4+
require 'aws-sdk-cloudwatchlogs'
5+
require 'optparse'
6+
require 'shellwords'
7+
8+
config = {}
9+
OptionParser.new do |opts|
10+
opts.banner = 'Usage: ecs_run.rb [options] [command or STDIN]'
11+
12+
opts.on('-c', '--cluster=CLUSTER', 'Cluster name') do |c|
13+
config[:cluster] = c
14+
end
15+
16+
opts.on('-s', '--service=SERVICE', 'Service name') do |s|
17+
config[:service] = s
18+
end
19+
20+
opts.on('-w', '--watch', 'Watch output') do |s|
21+
config[:watch] = true
22+
end
23+
24+
opts.on('-r', '--ruby', 'Run input as Ruby code with Rails runner (instead of shell command)') do |r|
25+
config[:ruby] = true
26+
end
27+
end.parse!
28+
raise OptionParser::MissingArgument, 'cluster' if config[:cluster].nil?
29+
raise OptionParser::MissingArgument, 'service' if config[:service].nil?
30+
31+
command = ARGV[0]
32+
unless command
33+
puts 'Type your command then press Ctrl+D' if STDIN.tty?
34+
puts 'Note - Ruby evaluation result is NOT automatically printed, use `p`' if config[:ruby]
35+
puts
36+
command = STDIN.read
37+
puts
38+
end
39+
command = "bundle exec rails runner #{command.shellescape}" if config[:ruby]
40+
41+
client = Aws::ECS::Client.new
42+
43+
resp = client.describe_services(
44+
cluster: config[:cluster],
45+
services: [
46+
config[:service]
47+
]
48+
)
49+
service = resp.services[0]
50+
51+
task_definition = client.describe_task_definition(task_definition: service.task_definition).task_definition
52+
53+
if task_definition.container_definitions.length > 1
54+
raise 'Running in tasks with more than one container is not yet supported'
55+
end
56+
57+
container_name = task_definition.container_definitions.first.name
58+
59+
vpc_config = service.deployments[0].network_configuration.awsvpc_configuration
60+
61+
subnet = vpc_config.subnets[0]
62+
security_group = vpc_config.security_groups[0]
63+
64+
task_response = client.run_task(
65+
cluster: config[:cluster],
66+
task_definition: service.task_definition,
67+
launch_type: 'FARGATE',
68+
overrides: {
69+
container_overrides: [
70+
{
71+
name: container_name,
72+
command: ['sh', '-c', command]
73+
}
74+
]
75+
},
76+
network_configuration: {
77+
awsvpc_configuration: {
78+
subnets: [subnet],
79+
security_groups: [security_group]
80+
}
81+
}
82+
)
83+
84+
task_arn = task_response.tasks[0].task_arn
85+
task_arn_parts = task_arn.split(':')
86+
task_region = task_arn_parts[3]
87+
task_id = task_arn_parts[5].split('/').last
88+
89+
puts "Task started. See it online at https://#{task_region}.console.aws.amazon.com/ecs/home?region=#{task_region}#/clusters/#{config[:cluster]}/tasks/#{task_id}/details"
90+
91+
exit unless config[:watch]
92+
93+
puts 'Watching task. Note - Ctrl+C will stop watching, but will NOT stop the task!'
94+
last_notified_status = ''
95+
96+
log_configuration = task_definition.container_definitions.first.log_configuration
97+
log_client = nil
98+
log_stream_name = nil
99+
log_token = nil
100+
if log_configuration.log_driver == 'awslogs'
101+
log_client = Aws::CloudWatchLogs::Client.new
102+
log_stream_name = "#{log_configuration.options['awslogs-stream-prefix']}/#{container_name}/#{task_id}"
103+
log_token = nil
104+
else
105+
puts 'Use `awslogs` log adapter to see the task output.'
106+
end
107+
108+
loop do
109+
task_status = client.describe_tasks(cluster: config[:cluster], tasks: [task_id]).tasks[0].last_status
110+
if task_status != last_notified_status
111+
puts "[#{Time.now}] Task status changed to #{task_status}"
112+
last_notified_status = task_status
113+
break if task_status == 'STOPPED'
114+
end
115+
116+
if log_client && %w[RUNNING DEPROVISIONING].include?(task_status)
117+
events_resp = log_client.get_log_events(
118+
log_group_name: log_configuration.options['awslogs-group'],
119+
log_stream_name: log_stream_name,
120+
start_from_head: true,
121+
next_token: log_token
122+
)
123+
events_resp.events.each do |event|
124+
puts "[#{Time.at(event.timestamp / 1000)}] #{event.message}"
125+
end
126+
log_token = events_resp.next_forward_token
127+
end
128+
sleep 1
129+
end

param_tool.rb

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#!/bin/env ruby
2+
13
require 'yaml'
24
require 'aws-sdk-ssm'
35
require 'optparse'

0 commit comments

Comments
 (0)