wrapbox 0.9.0 → 0.10.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.
- checksums.yaml +4 -4
- data/.dockerignore +0 -1
- data/.github/workflows/main.yml +30 -0
- data/README.md +94 -30
- data/lib/wrapbox/config_repository.rb +6 -1
- data/lib/wrapbox/configuration.rb +23 -6
- data/lib/wrapbox/log_fetcher/awslogs.rb +25 -13
- data/lib/wrapbox/runner/docker.rb +16 -2
- data/lib/wrapbox/runner/ecs/instance_manager.rb +92 -0
- data/lib/wrapbox/runner/ecs/task_waiter.rb +120 -0
- data/lib/wrapbox/runner/ecs.rb +174 -109
- data/lib/wrapbox/version.rb +1 -1
- data/lib/wrapbox.rb +11 -6
- data/wrapbox.gemspec +5 -3
- metadata +45 -16
- data/.travis.yml +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bdb5f8dc31647c10b941f003cc2c0c16127210e38e922abe1ed6f24a691ac497
|
4
|
+
data.tar.gz: 8fce00a8ada4301f82a079c57a51a9d85fb4502ae95b13ce0a0582d83f3eef70
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d49adcac17d1390776a19992e9636680617dda700f6084c8e6b06e17d300b133cf452ed7f80d50eea84c2e0b6b9e97121629b4e3255263726172e520d3d7dfe2
|
7
|
+
data.tar.gz: 3ef0163a2dfd7180f8dfbff4508f4c7171f7bfd29a828c71744523e1bcdc647553cb37aa9a353ab5c06a796224168a08c362662b5211ea770b062cd2eb3a43ad
|
data/.dockerignore
CHANGED
@@ -0,0 +1,30 @@
|
|
1
|
+
name: Testing on Ubuntu
|
2
|
+
on:
|
3
|
+
- push
|
4
|
+
- pull_request
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
runs-on: ${{ matrix.os }}
|
8
|
+
timeout-minutes: 10
|
9
|
+
strategy:
|
10
|
+
fail-fast: false
|
11
|
+
matrix:
|
12
|
+
ruby:
|
13
|
+
- 2.7
|
14
|
+
- 3.0
|
15
|
+
- 3.1
|
16
|
+
- 3.2
|
17
|
+
os:
|
18
|
+
- ubuntu-latest
|
19
|
+
name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }}
|
20
|
+
steps:
|
21
|
+
- uses: actions/checkout@v2
|
22
|
+
- uses: ruby/setup-ruby@v1
|
23
|
+
with:
|
24
|
+
ruby-version: ${{ matrix.ruby }}
|
25
|
+
- name: unit testing
|
26
|
+
env:
|
27
|
+
CI: true
|
28
|
+
run: |
|
29
|
+
bundle install --jobs 4 --retry 3
|
30
|
+
bundle exec rake spec
|
data/README.md
CHANGED
@@ -78,6 +78,13 @@ Wrapbox.run("TestJob", :perform, ["arg1", ["arg2", "arg3"]], config_name: :docke
|
|
78
78
|
Wrapbox.run_cmd(["ls ."], environments: [{name: "RAILS_ENV", value: "development"}])
|
79
79
|
```
|
80
80
|
|
81
|
+
If ECS runner cannot create task, it puts custom metric data to CloudWatch.
|
82
|
+
Custom metric data is `wrapbox/WaitingTaskCount` that has `ClusterName` dimension.
|
83
|
+
And, it retry launching until retry count reach `launch_retry`.
|
84
|
+
|
85
|
+
After task exited, Wrapbox checks main container exit code.
|
86
|
+
If exit code is not 0, Wrapbox raise error.
|
87
|
+
|
81
88
|
## Config
|
82
89
|
|
83
90
|
### Common
|
@@ -88,20 +95,25 @@ Wrapbox.run_cmd(["ls ."], environments: [{name: "RAILS_ENV", value: "development
|
|
88
95
|
|
89
96
|
### for ECS
|
90
97
|
|
91
|
-
| name
|
92
|
-
|
|
93
|
-
| cluster
|
94
|
-
| region
|
95
|
-
| container_definitions
|
96
|
-
| task_role_arn
|
97
|
-
| volumes
|
98
|
-
| placement_constraints
|
99
|
-
| placement_strategy
|
100
|
-
| launch_type
|
101
|
-
|
|
102
|
-
| network_configuration
|
103
|
-
|
|
104
|
-
|
|
98
|
+
| name | desc |
|
99
|
+
| -------------------------- | ------------------------------------------------ |
|
100
|
+
| cluster | target ECS cluster name |
|
101
|
+
| region | region of ECS cluster |
|
102
|
+
| container_definitions | see http://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#register_task_definition-instance_method |
|
103
|
+
| task_role_arn | see http://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html |
|
104
|
+
| volumes | see http://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#register_task_definition-instance_method |
|
105
|
+
| placement_constraints | see http://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#register_task_definition-instance_method |
|
106
|
+
| placement_strategy | see http://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#register_task_definition-instance_method |
|
107
|
+
| launch_type | see http://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#run_task-instance_method |
|
108
|
+
| network_mode | see http://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#register_task_definition-instance_method |
|
109
|
+
| network_configuration | see http://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#run_task-instance_method |
|
110
|
+
| capacity_provider_strategy | see https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ECS/Client.html#run_task-instance_method |
|
111
|
+
| cpu | see http://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#register_task_definition-instance_method |
|
112
|
+
| memory | see http://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#register_task_definition-instance_method |
|
113
|
+
| enable_ecs_managed_tags | see https://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#run_task-instance_method |
|
114
|
+
| tags | tags of task definitions. see also https://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#register_task_definition-instance_method |
|
115
|
+
| propagate_tags | specify `"TASK_DEFINITION"` if you want to propagate tags to tasks. see also https://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#run_task-instance_method |
|
116
|
+
| launch_instances | specify `launch_template` (required), `instance_type`, and `tag_specifications` for [Aws::EC2::Client#run_instances](https://docs.aws.amazon.com/sdkforruby/api/Aws/EC2/Client.html#run_instances-instance_method). You can also specify `wait_until_instance_terminated` (default: true) |
|
105
117
|
|
106
118
|
`WRAPBOX_CMD_INDEX` environment variable is available in `run_cmd` and you can distinguish logs from each command like below:
|
107
119
|
|
@@ -119,28 +131,59 @@ log_configuration:
|
|
119
131
|
| -------------------- | ----------------------------------------------------------- |
|
120
132
|
| container_definitions | only use `image`, `cpu`, `memory`, and `memory_reservation` |
|
121
133
|
| keep_container | If true, doesn't delete the container when the command ends |
|
122
|
-
| use_sudo | If true, invoke `sudo docker` command |
|
123
134
|
|
124
135
|
## API
|
125
136
|
|
126
|
-
|
137
|
+
### `Wrapbox.run`
|
127
138
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
139
|
+
```ruby
|
140
|
+
Wrapbox.run(class_name, method_name, args,
|
141
|
+
runner: nil, # The "runner" value is used in the configuration if it is nil.
|
142
|
+
config_name: nil, # "default" configuration is used if it is nil.
|
143
|
+
cluster: nil, # Available only for ECS runner. The "cluster" value in the configuration is used if it is nil.
|
144
|
+
launch_type: "EC2", # Available only for ECS runner. The "launch_type" value in the configuration is used if it is nil.
|
145
|
+
task_role_arn: nil, # Available only for ECS runner. The "task_role_arn" value in the configuration is used if it is nil.
|
146
|
+
execution_role_arn: nil, # Available only for ECS runner. The "execution_role_arn" value in the configuration is used if it is nil.
|
147
|
+
tags: nil, # Available only for ECS runner. The "tags" value in the configuration is used if it is nil.
|
148
|
+
propagate_tags: nil, # Available only for ECS runner. The "propagate_tags" value in the configuration is used if it is nil.
|
149
|
+
container_definition_overrides: {},
|
150
|
+
environments: [],
|
151
|
+
timeout: 3600 * 24, # Available only for ECS runner. # Available only for ECS runner.
|
152
|
+
launch_timeout: 60 * 10, # Available only for ECS runner.
|
153
|
+
launch_retry: 10, # Available only for ECS runner.
|
154
|
+
retry_interval: 1, # Available only for ECS runner.
|
155
|
+
retry_interval_multiplier: 2, # Available only for ECS runner.
|
156
|
+
max_retry_interval: 120, # Available only for ECS runner.
|
157
|
+
execution_retry: 0, # Available only for ECS runner.
|
158
|
+
keep_container: nil, # Available only for Docker runner. The "keep_container" value in the configuration is used if it is nil.
|
159
|
+
)
|
160
|
+
```
|
137
161
|
|
138
|
-
|
139
|
-
Custom metric data is `wrapbox/WaitingTaskCount` that has `ClusterName` dimension.
|
140
|
-
And, it retry launching until retry count reach `launch_retry`.
|
162
|
+
### `Wrapbox.run_cmd`
|
141
163
|
|
142
|
-
|
143
|
-
|
164
|
+
```ruby
|
165
|
+
Wrapbox.run_cmd(*cmd,
|
166
|
+
runner: nil, # The "runner" value is used in the configuration if it is nil.
|
167
|
+
config_name: nil, # "default" configuration is used if it is nil.
|
168
|
+
cluster: nil, # Available only for ECS runner. The "cluster" value in the configuration is used if it is nil.
|
169
|
+
launch_type: "EC2", # Available only for ECS runner. The "launch_type" value in the configuration is used if it is nil.
|
170
|
+
task_role_arn: nil, # Available only for ECS runner. The "task_role_arn" value in the configuration is used if it is nil.
|
171
|
+
execution_role_arn: nil, # Available only for ECS runner. The "execution_role_arn" value in the configuration is used if it is nil.
|
172
|
+
tags: nil, # Available only for ECS runner. The "tags" value in the configuration is used if it is nil.
|
173
|
+
propagate_tags: nil, # Available only for ECS runner. The "propagate_tags" value in the configuration is used if it is nil.
|
174
|
+
container_definition_overrides: {},
|
175
|
+
ignore_signal: false,
|
176
|
+
environments: [],
|
177
|
+
timeout: 3600 * 24, # Available only for ECS runner. # Available only for ECS runner.
|
178
|
+
launch_timeout: 60 * 10, # Available only for ECS runner.
|
179
|
+
launch_retry: 10, # Available only for ECS runner.
|
180
|
+
retry_interval: 1, # Available only for ECS runner.
|
181
|
+
retry_interval_multiplier: 2, # Available only for ECS runner.
|
182
|
+
max_retry_interval: 120, # Available only for ECS runner.
|
183
|
+
execution_retry: 0, # Available only for ECS runner.
|
184
|
+
keep_container: nil, # Available only for Docker runner. The "keep_container" value in the configuration is used if it is nil.
|
185
|
+
)
|
186
|
+
```
|
144
187
|
|
145
188
|
## Development
|
146
189
|
|
@@ -148,6 +191,27 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
148
191
|
|
149
192
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
150
193
|
|
194
|
+
### How to test
|
195
|
+
|
196
|
+
The following environment variables are required to run all tests.
|
197
|
+
|
198
|
+
Name | Description
|
199
|
+
-----|----------------
|
200
|
+
RUN_AWS_SPECS | Set "true" to run tests with `aws` set to true. You should also set credentials for AWS account to run ECS tasks.
|
201
|
+
ECS_CLUSTER | A cluster used in tests. "default" cluster is used if this variable is not set.
|
202
|
+
OVERRIDDEN_ECS_CLUSTER | A cluster used in tests that ensure `cluster` parameter.
|
203
|
+
LAUNCH_TEMPLATE_ID | A launch template used in tests that ensure `launch_instances` configuration.
|
204
|
+
|
205
|
+
|
206
|
+
```
|
207
|
+
env \
|
208
|
+
RUN_AWS_SPECS=true \
|
209
|
+
ECS_CLUSTER='some_cluster' \
|
210
|
+
OVERRIDDEN_ECS_CLUSTER='another_cluster' \
|
211
|
+
LAUNCH_TEMPLATE_ID=lt-xxxxxxxxxxxxxxxxx \
|
212
|
+
bundle exec rspec
|
213
|
+
```
|
214
|
+
|
151
215
|
## Contributing
|
152
216
|
|
153
217
|
Bug reports and pull requests are welcome on GitHub at https://github.com/reproio/wrapbox.
|
@@ -8,7 +8,12 @@ module Wrapbox
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def load_yaml(yaml_file)
|
11
|
-
|
11
|
+
file = ERB.new(File.read(yaml_file)).result
|
12
|
+
configs = if Gem::Version.new(Psych::VERSION) >= Gem::Version.new("4.0.0")
|
13
|
+
YAML.load(file, aliases: true)
|
14
|
+
else
|
15
|
+
YAML.load(file)
|
16
|
+
end
|
12
17
|
configs.each do |name, configuration|
|
13
18
|
load_config(name, configuration.merge("name" => name))
|
14
19
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require "active_support"
|
1
2
|
require "active_support/core_ext/hash"
|
2
3
|
require "active_support/core_ext/string"
|
3
4
|
|
@@ -17,6 +18,7 @@ module Wrapbox
|
|
17
18
|
:volumes,
|
18
19
|
:placement_constraints,
|
19
20
|
:placement_strategy,
|
21
|
+
:capacity_provider_strategy,
|
20
22
|
:launch_type,
|
21
23
|
:requires_compatibilities,
|
22
24
|
:task_definition,
|
@@ -28,7 +30,12 @@ module Wrapbox
|
|
28
30
|
:task_role_arn,
|
29
31
|
:execution_role_arn,
|
30
32
|
:keep_container,
|
31
|
-
:log_fetcher
|
33
|
+
:log_fetcher,
|
34
|
+
:tags,
|
35
|
+
:enable_ecs_managed_tags,
|
36
|
+
:propagate_tags,
|
37
|
+
:launch_instances,
|
38
|
+
:enable_execute_command,
|
32
39
|
) do
|
33
40
|
def self.load_config(config)
|
34
41
|
new(
|
@@ -46,6 +53,7 @@ module Wrapbox
|
|
46
53
|
config["volumes"]&.map(&:deep_symbolize_keys) || [],
|
47
54
|
config["placement_constraints"]&.map(&:deep_symbolize_keys) || [],
|
48
55
|
config["placement_strategy"]&.map(&:deep_symbolize_keys) || [],
|
56
|
+
config["capacity_provider_strategy"]&.map(&:deep_symbolize_keys) || [],
|
49
57
|
config["launch_type"],
|
50
58
|
config["requires_compatibilities"] || ["EC2"],
|
51
59
|
config["task_definition"]&.deep_symbolize_keys,
|
@@ -57,7 +65,12 @@ module Wrapbox
|
|
57
65
|
config["task_role_arn"],
|
58
66
|
config["execution_role_arn"],
|
59
67
|
config["keep_container"],
|
60
|
-
config["log_fetcher"]&.deep_symbolize_keys
|
68
|
+
config["log_fetcher"]&.deep_symbolize_keys,
|
69
|
+
config["tags"],
|
70
|
+
config["enable_ecs_managed_tags"],
|
71
|
+
config["propagate_tags"],
|
72
|
+
config["launch_instances"]&.deep_symbolize_keys,
|
73
|
+
config["enable_execute_command"]
|
61
74
|
)
|
62
75
|
end
|
63
76
|
|
@@ -67,19 +80,23 @@ module Wrapbox
|
|
67
80
|
super
|
68
81
|
end
|
69
82
|
|
70
|
-
def
|
83
|
+
def runner_class(overrided_runner = nil)
|
71
84
|
r = overrided_runner || runner
|
72
85
|
raise "#{r} is unsupported runner" unless AVAILABLE_RUNNERS.include?(r.to_sym)
|
73
86
|
require "wrapbox/runner/#{r}"
|
74
|
-
Wrapbox::Runner.const_get(r.to_s.camelcase)
|
87
|
+
Wrapbox::Runner.const_get(r.to_s.camelcase)
|
75
88
|
end
|
76
89
|
|
77
90
|
def run(class_name, method_name, args, runner: nil, **options)
|
78
|
-
|
91
|
+
klass = runner_class(runner)
|
92
|
+
overridable_options, parameters = klass.split_overridable_options_and_parameters(options)
|
93
|
+
klass.new(to_h.merge(overridable_options)).run(class_name, method_name, args, **parameters)
|
79
94
|
end
|
80
95
|
|
81
96
|
def run_cmd(*cmd, runner: nil, **options)
|
82
|
-
|
97
|
+
klass = runner_class(runner)
|
98
|
+
overridable_options, parameters = klass.split_overridable_options_and_parameters(options)
|
99
|
+
klass.new(to_h.merge(overridable_options)).run_cmd(*cmd, **parameters)
|
83
100
|
end
|
84
101
|
end
|
85
102
|
end
|
@@ -27,6 +27,14 @@ module Wrapbox
|
|
27
27
|
|
28
28
|
def run(task:)
|
29
29
|
@loop_thread = Thread.start do
|
30
|
+
# It smees that task.contaienrs is empty
|
31
|
+
# if capacity_provider_strategy is specified and there are no remaining capacity
|
32
|
+
while task.containers.empty?
|
33
|
+
Wrapbox.logger.warn("The task has no containers, so fetch it again")
|
34
|
+
sleep 10
|
35
|
+
task = ecs_client.describe_tasks(cluster: task.cluster_arn, tasks: [task.task_arn]).tasks.first
|
36
|
+
end
|
37
|
+
|
30
38
|
main_loop(task)
|
31
39
|
end
|
32
40
|
end
|
@@ -45,30 +53,34 @@ module Wrapbox
|
|
45
53
|
log_group_name: @log_group,
|
46
54
|
log_stream_names: log_stream_names,
|
47
55
|
filter_pattern: @filter_pattern,
|
48
|
-
interleaved: true,
|
49
56
|
}.compact
|
50
57
|
@max_timestamp = ((Time.now.to_f - 120) * 1000).round
|
51
58
|
|
52
59
|
until @stop do
|
53
60
|
filter_log_opts[:start_time] = @max_timestamp + 1
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
+
begin
|
62
|
+
client.filter_log_events(filter_log_opts).each do |r|
|
63
|
+
r.events.each do |ev|
|
64
|
+
next if @displayed_event_ids.member?(ev.event_id)
|
65
|
+
display_message(ev)
|
66
|
+
@displayed_event_ids[ev.event_id] = ev.timestamp
|
67
|
+
@max_timestamp = ev.timestamp if @max_timestamp < ev.timestamp
|
68
|
+
end
|
61
69
|
end
|
62
|
-
|
63
|
-
Thread.start do
|
70
|
+
|
64
71
|
@displayed_event_ids.each do |event_id, ts|
|
65
72
|
if ts < (Time.now.to_f - 600) * 1000
|
66
73
|
@displayed_event_ids.delete(event_id)
|
67
74
|
end
|
68
75
|
end
|
69
|
-
|
70
|
-
|
71
|
-
|
76
|
+
rescue Aws::CloudWatchLogs::Errors::ResourceNotFoundException
|
77
|
+
# Ignore the error because it is an error like "The specified log stream does not exist.",
|
78
|
+
# which occurs when the log stream hasn't been created yet, that is, the task hasn't started yet.
|
79
|
+
rescue Aws::CloudWatchLogs::Errors::ThrottlingException
|
80
|
+
Wrapbox.logger.warn("Failed to fetch logs due to Aws::CloudWatchLogs::Errors::ThrottlingException")
|
81
|
+
end
|
82
|
+
|
83
|
+
sleep @delay
|
72
84
|
end
|
73
85
|
end
|
74
86
|
|
@@ -4,6 +4,8 @@ require "docker"
|
|
4
4
|
require "thor"
|
5
5
|
require "shellwords"
|
6
6
|
|
7
|
+
require "wrapbox"
|
8
|
+
|
7
9
|
module Wrapbox
|
8
10
|
module Runner
|
9
11
|
class Docker
|
@@ -14,6 +16,17 @@ module Wrapbox
|
|
14
16
|
:container_definition,
|
15
17
|
:keep_container
|
16
18
|
|
19
|
+
def self.split_overridable_options_and_parameters(options)
|
20
|
+
opts = options.dup
|
21
|
+
overridable_options = {}
|
22
|
+
%i[keep_container].each do |key|
|
23
|
+
value = opts.delete(key)
|
24
|
+
overridable_options[key] = value if value
|
25
|
+
end
|
26
|
+
|
27
|
+
[overridable_options, opts]
|
28
|
+
end
|
29
|
+
|
17
30
|
def initialize(options)
|
18
31
|
@name = options[:name]
|
19
32
|
@container_definitions = options[:container_definition] ? [options[:container_definition]] : options[:container_definitions]
|
@@ -136,6 +149,7 @@ module Wrapbox
|
|
136
149
|
method_option :config_name, aliases: "-n", required: true, default: "default"
|
137
150
|
method_option :cpu, type: :numeric
|
138
151
|
method_option :memory, type: :numeric
|
152
|
+
method_option :working_directory, aliases: "-w", type: :string
|
139
153
|
method_option :environments, aliases: "-e"
|
140
154
|
method_option :ignore_signal, type: :boolean, default: false, desc: "Even if receive a signal (like TERM, INT, QUIT), Docker container continue running"
|
141
155
|
method_option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Verbose mode"
|
@@ -146,8 +160,8 @@ module Wrapbox
|
|
146
160
|
environments = options[:environments].to_s.split(/,\s*/).map { |kv| kv.split("=") }.map do |k, v|
|
147
161
|
{name: k, value: v}
|
148
162
|
end
|
149
|
-
if options[:cpu] || options[:memory]
|
150
|
-
container_definition_overrides = {cpu: options[:cpu], memory: options[:memory]}.reject { |_, v| v.nil? }
|
163
|
+
if options[:cpu] || options[:memory] || options[:working_directory]
|
164
|
+
container_definition_overrides = {cpu: options[:cpu], memory: options[:memory], working_directory: options[:working_directory]}.reject { |_, v| v.nil? }
|
151
165
|
else
|
152
166
|
container_definition_overrides = {}
|
153
167
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require "aws-sdk-ec2"
|
2
|
+
require "aws-sdk-ecs"
|
3
|
+
|
4
|
+
module Wrapbox
|
5
|
+
module Runner
|
6
|
+
class Ecs
|
7
|
+
class InstanceManager
|
8
|
+
def initialize(cluster, region, launch_template:, instance_type: nil, tag_specifications: nil, wait_until_instance_terminated: true)
|
9
|
+
@cluster = cluster
|
10
|
+
@region = region
|
11
|
+
@launch_template = launch_template
|
12
|
+
@instance_type = instance_type
|
13
|
+
@tag_specifications = tag_specifications
|
14
|
+
@wait_until_instance_terminated = wait_until_instance_terminated
|
15
|
+
@queue = Queue.new
|
16
|
+
@instance_ids = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def pop_ec2_instance_id
|
20
|
+
Wrapbox.logger.debug("Wait until a new container instance are registered in \"#{@cluster}\" cluster")
|
21
|
+
@queue.pop
|
22
|
+
end
|
23
|
+
|
24
|
+
def start_preparing_instances(count)
|
25
|
+
preparing_instance_ids = ec2_client.run_instances(
|
26
|
+
launch_template: @launch_template,
|
27
|
+
instance_type: @instance_type,
|
28
|
+
tag_specifications: @tag_specifications,
|
29
|
+
min_count: count,
|
30
|
+
max_count: count
|
31
|
+
).instances.map(&:instance_id)
|
32
|
+
@instance_ids.concat(preparing_instance_ids)
|
33
|
+
ec2_client.wait_until(:instance_running, instance_ids: preparing_instance_ids)
|
34
|
+
|
35
|
+
waiter = Aws::Waiters::Waiter.new(
|
36
|
+
max_attempts: 40,
|
37
|
+
delay: 15,
|
38
|
+
poller: Aws::Waiters::Poller.new(
|
39
|
+
operation_name: :list_container_instances,
|
40
|
+
acceptors: [
|
41
|
+
{
|
42
|
+
"expected" => true,
|
43
|
+
"matcher" => "path",
|
44
|
+
"state" => "success",
|
45
|
+
"argument" => "length(container_instance_arns) > `0`"
|
46
|
+
}
|
47
|
+
]
|
48
|
+
)
|
49
|
+
)
|
50
|
+
|
51
|
+
while preparing_instance_ids.size > 0
|
52
|
+
waiter.wait(client: ecs_client, params: { cluster: @cluster, filter: "ec2InstanceId in [#{preparing_instance_ids.join(",")}]" }).each do |resp|
|
53
|
+
ecs_client.describe_container_instances(cluster: @cluster, container_instances: resp.container_instance_arns).container_instances.each do |c|
|
54
|
+
preparing_instance_ids.delete(c.ec2_instance_id)
|
55
|
+
@queue << c.ec2_instance_id
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def terminate_instance(instance_id)
|
62
|
+
ec2_client.terminate_instances(instance_ids: [instance_id])
|
63
|
+
if @wait_until_instance_terminated
|
64
|
+
ec2_client.wait_until(:instance_terminated, instance_ids: [instance_id])
|
65
|
+
end
|
66
|
+
@instance_ids.delete(instance_id)
|
67
|
+
end
|
68
|
+
|
69
|
+
def terminate_all_instances
|
70
|
+
# Duplicate @instance_ids because other threads can change it
|
71
|
+
remaining_instance_ids = @instance_ids.dup
|
72
|
+
return if remaining_instance_ids.empty?
|
73
|
+
ec2_client.terminate_instances(instance_ids: remaining_instance_ids)
|
74
|
+
if @wait_until_instance_terminated
|
75
|
+
ec2_client.wait_until(:instance_terminated, instance_ids: remaining_instance_ids)
|
76
|
+
end
|
77
|
+
@instance_ids.clear
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def ecs_client
|
83
|
+
@ecs_client ||= Aws::ECS::Client.new({ region: @region }.reject { |_, v| v.nil? })
|
84
|
+
end
|
85
|
+
|
86
|
+
def ec2_client
|
87
|
+
@ec2_client ||= Aws::EC2::Client.new({ region: @region }.reject { |_, v| v.nil? })
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require "timeout"
|
2
|
+
|
3
|
+
require "aws-sdk-ecs"
|
4
|
+
|
5
|
+
module Wrapbox
|
6
|
+
module Runner
|
7
|
+
class Ecs
|
8
|
+
class TaskWaiter
|
9
|
+
MAX_DESCRIBABLE_TASK_COUNT = 100
|
10
|
+
|
11
|
+
class WaitFailure < StandardError; end
|
12
|
+
class TaskStopped < WaitFailure; end
|
13
|
+
class TaskMissing < WaitFailure; end
|
14
|
+
class UnknownFailure < WaitFailure; end
|
15
|
+
class WaitTimeout < WaitFailure; end
|
16
|
+
|
17
|
+
def initialize(cluster:, region:, delay:)
|
18
|
+
@cluster = cluster
|
19
|
+
@region = region
|
20
|
+
@task_arn_to_described_result = {}
|
21
|
+
@mutex = Mutex.new
|
22
|
+
@cv = ConditionVariable.new
|
23
|
+
Thread.new { update_described_results(delay) }
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return Aws::ECS::Types::Task
|
27
|
+
def wait_task_running(task_arn, timeout: 0)
|
28
|
+
Timeout.timeout(timeout) do
|
29
|
+
loop do
|
30
|
+
result = describe_task(task_arn)
|
31
|
+
if result[:failure]
|
32
|
+
case result[:failure].reason
|
33
|
+
when "MISSING"
|
34
|
+
raise TaskMissing
|
35
|
+
else
|
36
|
+
raise UnknownFailure
|
37
|
+
end
|
38
|
+
end
|
39
|
+
raise TaskStopped if result[:task].last_status == "STOPPED"
|
40
|
+
|
41
|
+
return result[:task] if result[:task].last_status == "RUNNING"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
rescue Timeout::Error
|
45
|
+
raise WaitTimeout
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return Aws::ECS::Types::Task
|
49
|
+
def wait_task_stopped(task_arn, timeout: 0)
|
50
|
+
Timeout.timeout(timeout) do
|
51
|
+
result = nil
|
52
|
+
loop do
|
53
|
+
result = describe_task(task_arn)
|
54
|
+
if result[:failure]
|
55
|
+
case result[:failure].reason
|
56
|
+
when "MISSING"
|
57
|
+
raise TaskMissing
|
58
|
+
else
|
59
|
+
raise UnknownFailure
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
return result[:task] if result[:task].last_status == "STOPPED"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
rescue Timeout::Error
|
67
|
+
raise WaitTimeout
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def describe_task(task_arn)
|
73
|
+
@mutex.synchronize do
|
74
|
+
@task_arn_to_described_result[task_arn] = nil
|
75
|
+
@cv.wait(@mutex)
|
76
|
+
@task_arn_to_described_result[task_arn]
|
77
|
+
end
|
78
|
+
ensure
|
79
|
+
@mutex.synchronize do
|
80
|
+
@task_arn_to_described_result.delete(task_arn)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def update_described_results(interval)
|
85
|
+
loop do
|
86
|
+
@mutex.synchronize do
|
87
|
+
unless @task_arn_to_described_result.empty?
|
88
|
+
begin
|
89
|
+
@task_arn_to_described_result.keys.each_slice(MAX_DESCRIBABLE_TASK_COUNT) do |task_arns|
|
90
|
+
resp = ecs_client.describe_tasks(cluster: @cluster, tasks: task_arns)
|
91
|
+
resp.tasks.each do |task|
|
92
|
+
@task_arn_to_described_result[task.task_arn] = { task: task }
|
93
|
+
end
|
94
|
+
resp.failures.each do |failure|
|
95
|
+
# failure.arn is like "arn:aws:ecs:<region>:<account-id>:task/<task-id>"
|
96
|
+
# even if task_arn is like "arn:aws:ecs:<region>:<account-id>:task/<cluster>/<task-id>"
|
97
|
+
prefix, suffix = failure.arn.split("/")
|
98
|
+
task_arn = task_arns.find { |a| a.start_with?(prefix) && a.end_with?(suffix) }
|
99
|
+
@task_arn_to_described_result[task_arn || failure.arn] = { failure: failure }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
@cv.broadcast
|
104
|
+
rescue Aws::ECS::Errors::ThrottlingException
|
105
|
+
Wrapbox.logger.warn("Failed to describe tasks due to Aws::ECS::Errors::ThrottlingException")
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
sleep interval
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def ecs_client
|
115
|
+
@ecs_client ||= Aws::ECS::Client.new({ region: @region }.reject { |_, v| v.nil? })
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|