broadside 1.1.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +4 -1
- data/bin/broadside +3 -3
- data/lib/broadside.rb +4 -3
- data/lib/broadside/configuration/{aws.rb → aws_config.rb} +0 -0
- data/lib/broadside/configuration/{base.rb → base_config.rb} +0 -0
- data/lib/broadside/configuration/{struct.rb → config_struct.rb} +0 -0
- data/lib/broadside/configuration/deploy_config.rb +18 -17
- data/lib/broadside/deploy/ecs_deploy.rb +113 -222
- data/lib/broadside/deploy/ecs_manager.rb +130 -0
- data/lib/broadside/utils.rb +1 -1
- data/lib/broadside/version.rb +1 -1
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 06c6097999d33e216468be90c482378b894496aa
|
4
|
+
data.tar.gz: e34ca4894bd642e4ea163f81f34f1b660b9689ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b583cb598536cd02b403d929ff237ea9632d124126b7f7abd03ab481f76de155a4ec489f83cfab771acc1bbf67f67fdee1112e3a1c462c6e2c1341890fae4161
|
7
|
+
data.tar.gz: aebd8291a1c8b21f7ada0f827834250aa601d7e0d0f199394d6a5523ee6905cde8b8b32f70e5bef1871333f3459dd9698165544d81f6fe86a360c194ecd65bb2
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
# 1.2.0
|
2
|
+
- [#32](https://github.com/lumoslabs/broadside/pull/32): Deploys will also update service configs defined in a deploy target (see full list in the [AWS Docs](https://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#create_service-instance_method))
|
3
|
+
- Updates additional container definition configs like cpu, memory. See full list in the [AWS Docs](https://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#register_task_definition-instance_method)
|
4
|
+
- [#24](https://github.com/lumoslabs/broadside/pull/24): Refactored most ECS-specific utility methods into a separate class
|
5
|
+
|
1
6
|
# 1.1.1
|
2
7
|
- [#25](https://github.com/lumoslabs/broadside/issues/25): Fix issue with undefined local variable 'ecs'
|
3
8
|
|
data/README.md
CHANGED
@@ -41,7 +41,10 @@ Broadside.configure do |config|
|
|
41
41
|
env_file: '../.env.staging'
|
42
42
|
},
|
43
43
|
# Example with a task_definition and service configuration which you use to bootstrap a service and
|
44
|
-
# initial task definition
|
44
|
+
# initial task definition. Accepts all the options AWS does - read their documentation for details:
|
45
|
+
#
|
46
|
+
# Service config: https://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#create_service-instance_method
|
47
|
+
# Task Definition Config: https://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#register_task_definition-instance_method
|
45
48
|
game_save_as_json_blob_stream: {
|
46
49
|
scale: 1,
|
47
50
|
command: ['java', '-cp', '*:.', 'path.to.MyClass'],
|
data/bin/broadside
CHANGED
@@ -27,7 +27,7 @@ def add_shared_deploy_configs(subcmd)
|
|
27
27
|
|
28
28
|
subcmd.action do |global_options, options, args|
|
29
29
|
_DeployObj = Kernel.const_get("Broadside::#{Broadside.config.deploy.type.capitalize}Deploy")
|
30
|
-
_DeployObj.new(options).
|
30
|
+
_DeployObj.new(options).public_send(subcmd.name)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
@@ -42,7 +42,7 @@ accept Fixnum do |val|
|
|
42
42
|
val.to_i
|
43
43
|
end
|
44
44
|
|
45
|
-
desc 'Bootstrap your service and task definition.'
|
45
|
+
desc 'Bootstrap your service and task definition from the configured definition.'
|
46
46
|
command :bootstrap do |b|
|
47
47
|
add_shared_deploy_configs(b)
|
48
48
|
end
|
@@ -168,7 +168,7 @@ on_error do |exception|
|
|
168
168
|
# false skips default error handling
|
169
169
|
case exception
|
170
170
|
when Broadside::MissingVariableError
|
171
|
-
error exception.message, "
|
171
|
+
error exception.message, "Run your last command with --help for more information."
|
172
172
|
false
|
173
173
|
else
|
174
174
|
true
|
data/lib/broadside.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
require 'broadside/error'
|
2
2
|
require 'broadside/utils'
|
3
3
|
require 'broadside/configuration'
|
4
|
-
require 'broadside/configuration/
|
5
|
-
require 'broadside/configuration/
|
6
|
-
require 'broadside/configuration/
|
4
|
+
require 'broadside/configuration/config_struct'
|
5
|
+
require 'broadside/configuration/aws_config'
|
6
|
+
require 'broadside/configuration/base_config'
|
7
7
|
require 'broadside/configuration/deploy_config'
|
8
8
|
require 'broadside/configuration/ecs_config'
|
9
9
|
require 'broadside/deploy'
|
10
10
|
require 'broadside/deploy/ecs_deploy'
|
11
|
+
require 'broadside/deploy/ecs_manager'
|
11
12
|
require 'broadside/version'
|
12
13
|
|
13
14
|
module Broadside
|
File without changes
|
File without changes
|
File without changes
|
@@ -31,7 +31,7 @@ module Broadside
|
|
31
31
|
scale: ->(target_attribute) { validate_types([Fixnum], target_attribute) },
|
32
32
|
env_file: ->(target_attribute) { validate_types([String], target_attribute) },
|
33
33
|
command: ->(target_attribute) { validate_types([Array, NilClass], target_attribute) },
|
34
|
-
predeploy_commands: ->(target_attribute) {
|
34
|
+
predeploy_commands: ->(target_attribute) { validate_predeploy_commands(target_attribute) },
|
35
35
|
service_config: ->(target_attribute) { validate_types([Hash, NilClass], target_attribute) },
|
36
36
|
task_definition_config: ->(target_attribute) { validate_types([Hash, NilClass], target_attribute) }
|
37
37
|
}
|
@@ -57,24 +57,24 @@ module Broadside
|
|
57
57
|
# Checks existence of provided target
|
58
58
|
def validate_targets!
|
59
59
|
@targets.each do |target, configuration|
|
60
|
-
TARGET_ATTRIBUTE_VALIDATIONS.
|
60
|
+
invalid_messages = TARGET_ATTRIBUTE_VALIDATIONS.map do |var, validation|
|
61
61
|
message = validation.call(configuration[var])
|
62
|
+
message.nil? ? nil : "Deploy target '#{@target}' parameter '#{var}' is invalid: #{message}"
|
63
|
+
end.compact
|
62
64
|
|
63
|
-
|
64
|
-
|
65
|
-
end
|
65
|
+
unless invalid_messages.empty?
|
66
|
+
raise ArgumentError, invalid_messages.join("\n")
|
66
67
|
end
|
67
68
|
end
|
68
69
|
|
69
70
|
unless @targets.has_key?(@target)
|
70
|
-
|
71
|
+
raise ArgumentError, "Could not find deploy target #{@target} in configuration !"
|
71
72
|
end
|
72
73
|
end
|
73
74
|
|
74
75
|
# Loads deploy target data using provided target
|
75
76
|
def load_target!
|
76
77
|
validate_targets!
|
77
|
-
|
78
78
|
env_file = Pathname.new(@targets[@target][:env_file])
|
79
79
|
|
80
80
|
unless env_file.absolute?
|
@@ -84,13 +84,14 @@ module Broadside
|
|
84
84
|
|
85
85
|
if env_file.exist?
|
86
86
|
vars = Dotenv.load(env_file)
|
87
|
-
@env_vars = vars.map { |k,v| {'name'=> k, 'value' => v } }
|
87
|
+
@env_vars = vars.map { |k, v| { 'name'=> k, 'value' => v } }
|
88
88
|
else
|
89
|
-
|
89
|
+
raise ArgumentError, "Could not find file '#{env_file}' for loading environment variables !"
|
90
90
|
end
|
91
91
|
|
92
92
|
@scale ||= @targets[@target][:scale]
|
93
93
|
@command = @targets[@target][:command]
|
94
|
+
# TODO: what's up with predeploy_commands. ||= []?
|
94
95
|
@predeploy_commands = @targets[@target][:predeploy_commands] if @targets[@target][:predeploy_commands]
|
95
96
|
@service_config = @targets[@target][:service_config]
|
96
97
|
@task_definition_config = @targets[@target][:task_definition_config]
|
@@ -99,18 +100,18 @@ module Broadside
|
|
99
100
|
private
|
100
101
|
|
101
102
|
def self.validate_types(types, target_attribute)
|
102
|
-
|
103
|
-
|
103
|
+
if types.include?(target_attribute.class)
|
104
|
+
nil
|
105
|
+
else
|
106
|
+
"'#{target_attribute}' must be of type [#{types.join('|')}], got '#{target_attribute.class}' !"
|
104
107
|
end
|
105
|
-
|
106
|
-
nil
|
107
108
|
end
|
108
109
|
|
109
|
-
def self.
|
110
|
-
return nil if
|
111
|
-
return 'predeploy_commands must be an array' unless
|
110
|
+
def self.validate_predeploy_commands(commands)
|
111
|
+
return nil if commands.nil?
|
112
|
+
return 'predeploy_commands must be an array' unless commands.is_a?(Array)
|
112
113
|
|
113
|
-
messages =
|
114
|
+
messages = commands.reject { |cmd| cmd.is_a?(Array) }.map do |command|
|
114
115
|
"predeploy_command '#{command}' must be an array" unless command.is_a?(Array)
|
115
116
|
end
|
116
117
|
messages.empty? ? nil : messages.join(', ')
|
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'active_support/core_ext/hash'
|
2
1
|
require 'aws-sdk'
|
3
2
|
require 'open3'
|
4
3
|
require 'pp'
|
@@ -7,8 +6,6 @@ require 'shellwords'
|
|
7
6
|
|
8
7
|
module Broadside
|
9
8
|
class EcsDeploy < Deploy
|
10
|
-
|
11
|
-
DEFAULT_DESIRED_COUNT = 0
|
12
9
|
DEFAULT_CONTAINER_DEFINITION = {
|
13
10
|
cpu: 1,
|
14
11
|
essential: true,
|
@@ -22,7 +19,13 @@ module Broadside
|
|
22
19
|
|
23
20
|
def deploy
|
24
21
|
super do
|
25
|
-
|
22
|
+
unless EcsManager.service_exists?(config.ecs.cluster, family)
|
23
|
+
exception "No service for #{family}! Please bootstrap or manually configure the service."
|
24
|
+
end
|
25
|
+
unless EcsManager.get_latest_task_definition_arn(family)
|
26
|
+
exception "No task definition for '#{family}'! Please bootstrap or manually configure the task definition."
|
27
|
+
end
|
28
|
+
|
26
29
|
update_task_revision
|
27
30
|
|
28
31
|
begin
|
@@ -37,36 +40,49 @@ module Broadside
|
|
37
40
|
error 'Deploy failed! Rolling back...'
|
38
41
|
rollback(1)
|
39
42
|
error 'Deployment did not finish successfully.'
|
40
|
-
raise
|
43
|
+
raise e
|
41
44
|
end
|
42
45
|
end
|
43
46
|
end
|
44
47
|
|
45
48
|
def bootstrap
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
+
if EcsManager.get_latest_task_definition_arn(family)
|
50
|
+
info("Task definition for #{family} already exists.")
|
51
|
+
else
|
52
|
+
unless @deploy_config.task_definition_config
|
53
|
+
raise ArgumentError, "No first task definition and no :task_definition_config in '#{family}' configuration"
|
54
|
+
end
|
49
55
|
|
50
56
|
info "Creating an initial task definition for '#{family}' from the config..."
|
51
|
-
|
57
|
+
|
58
|
+
EcsManager.ecs.register_task_definition(
|
59
|
+
@deploy_config.task_definition_config.merge(
|
60
|
+
family: family,
|
61
|
+
container_definitions: [DEFAULT_CONTAINER_DEFINITION.merge(container_definition)]
|
62
|
+
)
|
63
|
+
)
|
52
64
|
end
|
53
65
|
|
54
|
-
|
55
|
-
|
66
|
+
if EcsManager.service_exists?(config.ecs.cluster, family)
|
67
|
+
info("Service for #{family} already exists.")
|
68
|
+
else
|
69
|
+
unless @deploy_config.service_config
|
70
|
+
raise ArgumentError, "Service doesn't exist and no :service_config in '#{family}' configuration"
|
71
|
+
end
|
56
72
|
|
57
73
|
info "Service '#{family}' doesn't exist, creating..."
|
58
|
-
create_service(family, @deploy_config.service_config)
|
74
|
+
EcsManager.create_service(config.ecs.cluster, family, @deploy_config.service_config)
|
59
75
|
end
|
60
76
|
end
|
61
77
|
|
62
78
|
def rollback(count = @deploy_config.rollback)
|
63
79
|
super do
|
64
80
|
begin
|
65
|
-
|
81
|
+
EcsManager.deregister_last_n_tasks_definitions(family, count)
|
66
82
|
update_service
|
67
|
-
rescue StandardError
|
83
|
+
rescue StandardError
|
68
84
|
error 'Rollback failed to complete!'
|
69
|
-
raise
|
85
|
+
raise
|
70
86
|
end
|
71
87
|
end
|
72
88
|
end
|
@@ -84,7 +100,7 @@ module Broadside
|
|
84
100
|
begin
|
85
101
|
run_command(@deploy_config.command)
|
86
102
|
ensure
|
87
|
-
|
103
|
+
EcsManager.deregister_last_n_tasks_definitions(family, 1)
|
88
104
|
end
|
89
105
|
end
|
90
106
|
end
|
@@ -95,22 +111,19 @@ module Broadside
|
|
95
111
|
update_task_revision
|
96
112
|
|
97
113
|
begin
|
98
|
-
@deploy_config.predeploy_commands.each
|
99
|
-
run_command(command)
|
100
|
-
end
|
114
|
+
@deploy_config.predeploy_commands.each { |command| run_command(command) }
|
101
115
|
ensure
|
102
|
-
|
116
|
+
EcsManager.deregister_last_n_tasks_definitions(family, 1)
|
103
117
|
end
|
104
118
|
end
|
105
119
|
end
|
106
120
|
|
107
121
|
def status
|
108
122
|
super do
|
109
|
-
|
110
|
-
ips = get_running_instance_ips
|
123
|
+
ips = EcsManager.get_running_instance_ips(config.ecs.cluster, family)
|
111
124
|
info "\n---------------",
|
112
125
|
"\nDeployed task definition information:\n",
|
113
|
-
Rainbow(PP.pp(
|
126
|
+
Rainbow(PP.pp(EcsManager.get_latest_task_definition(family), '')).blue,
|
114
127
|
"\nPrivate ips of instances running containers:\n",
|
115
128
|
Rainbow(ips.join(' ')).blue,
|
116
129
|
"\n\nssh command:\n#{Rainbow(gen_ssh_cmd(ips.first)).cyan}",
|
@@ -120,7 +133,7 @@ module Broadside
|
|
120
133
|
|
121
134
|
def logtail
|
122
135
|
super do
|
123
|
-
ip =
|
136
|
+
ip = get_running_instance_ip
|
124
137
|
debug "Tailing logs for running container at ip #{ip}..."
|
125
138
|
search_pattern = Shellwords.shellescape(family)
|
126
139
|
cmd = "docker logs -f --tail=10 `docker ps -n 1 --quiet --filter name=#{search_pattern}`"
|
@@ -131,7 +144,7 @@ module Broadside
|
|
131
144
|
|
132
145
|
def ssh
|
133
146
|
super do
|
134
|
-
ip =
|
147
|
+
ip = get_running_instance_ip
|
135
148
|
debug "Establishing an SSH connection to ip #{ip}..."
|
136
149
|
exec gen_ssh_cmd(ip)
|
137
150
|
end
|
@@ -139,7 +152,7 @@ module Broadside
|
|
139
152
|
|
140
153
|
def bash
|
141
154
|
super do
|
142
|
-
ip =
|
155
|
+
ip = get_running_instance_ip
|
143
156
|
debug "Running bash for running container at ip #{ip}..."
|
144
157
|
search_pattern = Shellwords.shellescape(family)
|
145
158
|
cmd = "docker exec -i -t `docker ps -n 1 --quiet --filter name=#{search_pattern}` bash"
|
@@ -150,141 +163,100 @@ module Broadside
|
|
150
163
|
|
151
164
|
private
|
152
165
|
|
153
|
-
|
154
|
-
|
155
|
-
get_task_definition_arns.last(count).each do |td_id|
|
156
|
-
ecs_client.deregister_task_definition({task_definition: td_id})
|
157
|
-
debug "Deregistered #{td_id}"
|
158
|
-
end
|
166
|
+
def get_running_instance_ip
|
167
|
+
EcsManager.get_running_instance_ips(config.ecs.cluster, family).fetch(@deploy_config.instance)
|
159
168
|
end
|
160
169
|
|
161
|
-
#
|
170
|
+
# Creates a new task revision using current directory's env vars, provided tag, and configured options.
|
171
|
+
# Currently can only handle a single container definition.
|
162
172
|
def update_task_revision
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
container_def[:command] = @deploy_config.command
|
169
|
-
end
|
170
|
-
|
171
|
-
debug "Creating a new task definition..."
|
172
|
-
new_task_def_id = ecs_client.register_task_definition(new_task_def).task_definition.task_definition_arn
|
173
|
-
debug "Successfully created #{new_task_def_id}"
|
174
|
-
end
|
175
|
-
|
176
|
-
def create_service(name, options = {})
|
177
|
-
ecs_client.create_service(
|
178
|
-
{
|
179
|
-
cluster: config.ecs.cluster,
|
180
|
-
desired_count: DEFAULT_DESIRED_COUNT,
|
181
|
-
service_name: name,
|
182
|
-
task_definition: name
|
183
|
-
}.deep_merge(options)
|
173
|
+
revision = EcsManager.get_latest_task_definition(family).except(
|
174
|
+
:requires_attributes,
|
175
|
+
:revision,
|
176
|
+
:status,
|
177
|
+
:task_definition_arn
|
184
178
|
)
|
185
|
-
|
179
|
+
updatable_container_definitions = revision[:container_definitions].select { |c| c[:name] == family }
|
180
|
+
exception "Can only update one container definition!" if updatable_container_definitions.size != 1
|
186
181
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
name: name,
|
191
|
-
command: @deploy_config.command,
|
192
|
-
environment: @deploy_config.env_vars,
|
193
|
-
image: image_tag,
|
194
|
-
).merge(options[:container_definitions].first || {})
|
182
|
+
# Deep merge doesn't work well with arrays (e.g. :container_definitions), so build the container first.
|
183
|
+
updatable_container_definitions.first.merge!(container_definition)
|
184
|
+
revision.deep_merge!((@deploy_config.task_definition_config || {}).except(:container_definitions))
|
195
185
|
|
196
|
-
|
197
|
-
|
198
|
-
)
|
186
|
+
task_definition = EcsManager.ecs.register_task_definition(revision).task_definition
|
187
|
+
debug "Successfully created #{task_definition.task_definition_arn}"
|
199
188
|
end
|
200
189
|
|
201
190
|
# reloads the service using the latest task definition
|
202
191
|
def update_service
|
203
|
-
|
204
|
-
debug "Updating #{family} with scale=#{@deploy_config.scale} using task #{
|
205
|
-
|
192
|
+
task_definition_arn = EcsManager.get_latest_task_definition_arn(family)
|
193
|
+
debug "Updating #{family} with scale=#{@deploy_config.scale} using task #{task_definition_arn}..."
|
194
|
+
|
195
|
+
update_service_response = EcsManager.ecs.update_service({
|
206
196
|
cluster: config.ecs.cluster,
|
197
|
+
desired_count: @deploy_config.scale,
|
207
198
|
service: family,
|
208
|
-
task_definition:
|
209
|
-
|
210
|
-
})
|
199
|
+
task_definition: task_definition_arn
|
200
|
+
}.deep_merge(@deploy_config.service_config || {}))
|
211
201
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
end
|
202
|
+
unless update_service_response.successful?
|
203
|
+
exception('Failed to update service during deploy.', update_service_response.pretty_inspect)
|
204
|
+
end
|
205
|
+
|
206
|
+
EcsManager.ecs.wait_until(:services_stable, { cluster: config.ecs.cluster, services: [family] }) do |w|
|
207
|
+
w.max_attempts = @deploy_config.timeout ? @deploy_config.timeout / config.ecs.poll_frequency : nil
|
208
|
+
w.delay = config.ecs.poll_frequency
|
209
|
+
seen_event = nil
|
210
|
+
|
211
|
+
w.before_wait do |attempt, response|
|
212
|
+
debug "(#{attempt}/#{w.max_attempts}) Polling ECS for events..."
|
213
|
+
# skip first event since it doesn't apply to current request
|
214
|
+
if response.services[0].events.first && response.services[0].events.first.id != seen_event && attempt > 1
|
215
|
+
seen_event = response.services[0].events.first.id
|
216
|
+
debug(response.services[0].events.first.message)
|
228
217
|
end
|
229
|
-
rescue Aws::Waiters::Errors::TooManyAttemptsError
|
230
|
-
exception 'Deploy did not finish in the expected amount of time.'
|
231
218
|
end
|
232
|
-
else
|
233
|
-
exception 'Failed to update service during deploy.'
|
234
219
|
end
|
235
220
|
end
|
236
221
|
|
237
222
|
def run_command(command)
|
238
223
|
command_name = command.join(' ')
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
if resp.successful?
|
255
|
-
task_id = resp.tasks[0].task_arn
|
256
|
-
debug "Launched #{command_name} task #{task_id}"
|
257
|
-
debug "Waiting for #{command_name} to complete..."
|
258
|
-
ecs_client.wait_until(:tasks_stopped, {cluster: config.ecs.cluster, tasks: [task_id]}) do |w|
|
259
|
-
w.max_attempts = nil
|
260
|
-
w.delay = config.ecs.poll_frequency
|
261
|
-
w.before_attempt do |attempt|
|
262
|
-
debug "Attempt #{attempt}: waiting for #{command_name} to complete..."
|
263
|
-
end
|
264
|
-
end
|
265
|
-
debug 'Task finished running, getting logs...'
|
266
|
-
info "#{command_name} task container logs:\n#{get_container_logs(task_id)}"
|
267
|
-
if (code = get_task_exit_code(task_id)) == 0
|
268
|
-
debug "#{command_name} task #{task_id} exited with status code 0"
|
269
|
-
else
|
270
|
-
exception "#{command_name} task #{task_id} exited with a non-zero status code #{code}!"
|
224
|
+
run_task_response = EcsManager.run_task(config.ecs.cluster, family, command)
|
225
|
+
|
226
|
+
unless run_task_response.successful? && run_task_response.tasks.try(:[], 0)
|
227
|
+
exception("Failed to run #{command_name} task.", run_task_response.pretty_inspect)
|
228
|
+
end
|
229
|
+
|
230
|
+
task_arn = run_task_response.tasks[0].task_arn
|
231
|
+
debug "Launched #{command_name} task #{task_arn}, waiting for completion..."
|
232
|
+
|
233
|
+
EcsManager.ecs.wait_until(:tasks_stopped, { cluster: config.ecs.cluster, tasks: [task_arn] }) do |w|
|
234
|
+
w.max_attempts = nil
|
235
|
+
w.delay = config.ecs.poll_frequency
|
236
|
+
w.before_attempt do |attempt|
|
237
|
+
debug "Attempt #{attempt}: waiting for #{command_name} to complete..."
|
271
238
|
end
|
239
|
+
end
|
240
|
+
|
241
|
+
info "#{command_name} task container logs:\n#{get_container_logs(task_arn)}"
|
242
|
+
|
243
|
+
if (code = EcsManager.get_task_exit_code(config.ecs.cluster, task_arn, family)) == 0
|
244
|
+
debug "#{command_name} task #{task_arn} exited with status code 0"
|
272
245
|
else
|
273
|
-
|
246
|
+
exception "#{command_name} task #{task_arn} exited with a non-zero status code #{code}!"
|
274
247
|
end
|
275
248
|
end
|
276
249
|
|
277
|
-
def get_container_logs(
|
278
|
-
ip = get_running_instance_ips(
|
250
|
+
def get_container_logs(task_arn)
|
251
|
+
ip = EcsManager.get_running_instance_ips(config.ecs.cluster, family, task_arn).first
|
279
252
|
debug "Found ip of container instance: #{ip}"
|
280
253
|
|
281
|
-
find_container_id_cmd = "#{gen_ssh_cmd(ip)} \"docker ps -aqf 'label=com.amazonaws.ecs.task-arn=#{
|
254
|
+
find_container_id_cmd = "#{gen_ssh_cmd(ip)} \"docker ps -aqf 'label=com.amazonaws.ecs.task-arn=#{task_arn}'\""
|
282
255
|
debug "Running command to find container id:\n#{find_container_id_cmd}"
|
283
256
|
container_id = `#{find_container_id_cmd}`.strip
|
284
257
|
|
285
258
|
get_container_logs_cmd = "#{gen_ssh_cmd(ip)} \"docker logs #{container_id}\""
|
286
|
-
debug "Running command to get logs of container #{container_id}
|
287
|
-
"\n#{get_container_logs_cmd}"
|
259
|
+
debug "Running command to get logs of container #{container_id}:\n#{get_container_logs_cmd}"
|
288
260
|
|
289
261
|
logs = nil
|
290
262
|
Open3.popen3(get_container_logs_cmd) do |_, stdout, stderr, _|
|
@@ -293,99 +265,18 @@ module Broadside
|
|
293
265
|
logs
|
294
266
|
end
|
295
267
|
|
296
|
-
def
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
end
|
301
|
-
|
302
|
-
def get_running_instance_ips(task_ids = nil)
|
303
|
-
task_arns = nil
|
304
|
-
if task_ids.nil?
|
305
|
-
task_arns = get_task_arns
|
306
|
-
if task_arns.empty?
|
307
|
-
exception "No running tasks found for '#{family}' on cluster '#{config.ecs.cluster}' !"
|
308
|
-
end
|
309
|
-
elsif task_ids.class == String
|
310
|
-
task_arns = [task_ids]
|
311
|
-
else
|
312
|
-
task_arns = task_ids
|
313
|
-
end
|
314
|
-
|
315
|
-
tasks = ecs_client.describe_tasks({cluster: config.ecs.cluster, tasks: task_arns}).tasks
|
316
|
-
container_instance_arns = tasks.map { |t| t.container_instance_arn }
|
317
|
-
container_instances = ecs_client.describe_container_instances({
|
318
|
-
cluster: config.ecs.cluster, container_instances: container_instance_arns
|
319
|
-
}).container_instances
|
320
|
-
ec2_instance_ids = container_instances.map { |ci| ci.ec2_instance_id }
|
321
|
-
reservations = ec2_client.describe_instances({instance_ids: ec2_instance_ids}).reservations
|
322
|
-
instances = reservations.map { |r| r.instances }.flatten
|
323
|
-
|
324
|
-
instances.map { |i| i.private_ip_address }
|
325
|
-
end
|
326
|
-
|
327
|
-
def get_latest_task_def
|
328
|
-
ecs_client.describe_task_definition({task_definition: get_latest_task_def_id}).task_definition.to_h
|
329
|
-
end
|
330
|
-
|
331
|
-
def get_task_arns
|
332
|
-
all_results(:list_tasks, :task_arns, { cluster: config.ecs.cluster, family: family })
|
333
|
-
end
|
334
|
-
|
335
|
-
def get_task_definition_arns
|
336
|
-
all_results(:list_task_definitions, :task_definition_arns, { family_prefix: family })
|
337
|
-
end
|
338
|
-
|
339
|
-
def list_task_definition_families
|
340
|
-
all_results(:list_task_definition_families, :families)
|
341
|
-
end
|
342
|
-
|
343
|
-
def list_services
|
344
|
-
all_results(:list_services, :service_arns, { cluster: config.ecs.cluster })
|
345
|
-
end
|
346
|
-
|
347
|
-
def get_latest_task_def_id
|
348
|
-
get_task_definition_arns.last
|
349
|
-
end
|
350
|
-
|
351
|
-
def create_new_task_revision
|
352
|
-
task_def = get_latest_task_def
|
353
|
-
task_def.delete(:task_definition_arn)
|
354
|
-
task_def.delete(:requires_attributes)
|
355
|
-
task_def.delete(:revision)
|
356
|
-
task_def.delete(:status)
|
357
|
-
task_def
|
358
|
-
end
|
359
|
-
|
360
|
-
def service_exists?
|
361
|
-
services = ecs_client.describe_services({ cluster: config.ecs.cluster, services: [family] })
|
362
|
-
services.failures.empty? && !services.services.empty?
|
363
|
-
end
|
364
|
-
|
365
|
-
def ecs_client
|
366
|
-
@ecs_client ||= Aws::ECS::Client.new({
|
367
|
-
region: config.aws.region,
|
368
|
-
credentials: config.aws.credentials
|
369
|
-
})
|
370
|
-
end
|
371
|
-
|
372
|
-
def ec2_client
|
373
|
-
@ec2_client ||= Aws::EC2::Client.new({
|
374
|
-
region: config.aws.region,
|
375
|
-
credentials: config.aws.credentials
|
376
|
-
})
|
377
|
-
end
|
378
|
-
|
379
|
-
def all_results(method, key, args = {})
|
380
|
-
page = ecs_client.public_send(method, args)
|
381
|
-
results = page.send(key)
|
382
|
-
|
383
|
-
while page.next_token
|
384
|
-
page = ecs_client.public_send(method, args.merge(next_token: page.next_token))
|
385
|
-
results += page.send(key)
|
268
|
+
def container_definition
|
269
|
+
configured_containers = (@deploy_config.task_definition_config || {})[:container_definitions]
|
270
|
+
if configured_containers && configured_containers.size > 1
|
271
|
+
raise ArgumentError, 'Creating > 1 container definition not supported yet'
|
386
272
|
end
|
387
273
|
|
388
|
-
|
274
|
+
(configured_containers.try(:first) || {}).merge(
|
275
|
+
name: family,
|
276
|
+
command: @deploy_config.command,
|
277
|
+
environment: @deploy_config.env_vars,
|
278
|
+
image: image_tag
|
279
|
+
)
|
389
280
|
end
|
390
281
|
end
|
391
|
-
end
|
282
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'active_support/core_ext/hash'
|
2
|
+
require 'active_support/core_ext/array'
|
3
|
+
|
4
|
+
module Broadside
|
5
|
+
class EcsManager
|
6
|
+
DEFAULT_DESIRED_COUNT = 0
|
7
|
+
|
8
|
+
class << self
|
9
|
+
include Utils
|
10
|
+
|
11
|
+
def ecs
|
12
|
+
@ecs_client ||= Aws::ECS::Client.new(
|
13
|
+
region: Broadside.config.aws.region,
|
14
|
+
credentials: Broadside.config.aws.credentials
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_service(cluster, name, options = {})
|
19
|
+
ecs.create_service(
|
20
|
+
{
|
21
|
+
cluster: cluster,
|
22
|
+
desired_count: DEFAULT_DESIRED_COUNT,
|
23
|
+
service_name: name,
|
24
|
+
task_definition: name
|
25
|
+
}.deep_merge(options)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
# removes latest n task definitions
|
30
|
+
def deregister_last_n_tasks_definitions(name, count)
|
31
|
+
get_task_definition_arns(name).last(count).each do |arn|
|
32
|
+
ecs.deregister_task_definition(task_definition: arn)
|
33
|
+
debug "Deregistered #{arn}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_latest_task_definition(name)
|
38
|
+
return nil unless get_latest_task_definition_arn(name)
|
39
|
+
ecs.describe_task_definition(task_definition: get_latest_task_definition_arn(name)).task_definition.to_h
|
40
|
+
end
|
41
|
+
|
42
|
+
def get_latest_task_definition_arn(name)
|
43
|
+
get_task_definition_arns(name).last
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_running_instance_ips(cluster, family, task_arns = nil)
|
47
|
+
task_arns = task_arns ? Array.wrap(task_arns) : get_task_arns(cluster, family)
|
48
|
+
exception "No running tasks found for '#{family}' on cluster '#{cluster}'!" if task_arns.empty?
|
49
|
+
|
50
|
+
tasks = ecs.describe_tasks(cluster: cluster, tasks: task_arns).tasks
|
51
|
+
container_instances = ecs.describe_container_instances(
|
52
|
+
cluster: cluster,
|
53
|
+
container_instances: tasks.map(&:container_instance_arn),
|
54
|
+
).container_instances
|
55
|
+
ec2_instance_ids = container_instances.map(&:ec2_instance_id)
|
56
|
+
|
57
|
+
reservations = ec2_client.describe_instances(instance_ids: ec2_instance_ids).reservations
|
58
|
+
instances = reservations.map(&:instances).flatten
|
59
|
+
instances.map(&:private_ip_address)
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_task_arns(cluster, family)
|
63
|
+
all_results(:list_tasks, :task_arns, { cluster: cluster, family: family })
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_task_definition_arns(family)
|
67
|
+
all_results(:list_task_definitions, :task_definition_arns, { family_prefix: family })
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_task_exit_code(cluster, task_arn, name)
|
71
|
+
task = ecs.describe_tasks({ cluster: cluster, tasks: [task_arn] }).tasks.first
|
72
|
+
container = task.containers.select { |c| c.name == name }.first
|
73
|
+
container.exit_code
|
74
|
+
end
|
75
|
+
|
76
|
+
def list_task_definition_families
|
77
|
+
all_results(:list_task_definition_families, :families)
|
78
|
+
end
|
79
|
+
|
80
|
+
def list_services(cluster)
|
81
|
+
all_results(:list_services, :service_arns, { cluster: cluster })
|
82
|
+
end
|
83
|
+
|
84
|
+
def run_task(cluster, name, command)
|
85
|
+
fail ArgumentError, "#{command} must be an array" unless command.is_a?(Array)
|
86
|
+
|
87
|
+
ecs.run_task(
|
88
|
+
cluster: cluster,
|
89
|
+
task_definition: get_latest_task_definition_arn(name),
|
90
|
+
overrides: {
|
91
|
+
container_overrides: [
|
92
|
+
{
|
93
|
+
name: name,
|
94
|
+
command: command
|
95
|
+
}
|
96
|
+
]
|
97
|
+
},
|
98
|
+
count: 1,
|
99
|
+
started_by: "before_deploy:#{command.join(' ')}"[0...36]
|
100
|
+
)
|
101
|
+
end
|
102
|
+
|
103
|
+
def service_exists?(cluster, family)
|
104
|
+
services = ecs.describe_services(cluster: cluster, services: [family])
|
105
|
+
services.failures.empty? && services.services.any?
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def all_results(method, key, args = {})
|
111
|
+
page = ecs.public_send(method, args)
|
112
|
+
results = page.public_send(key)
|
113
|
+
|
114
|
+
while page.next_token
|
115
|
+
page = ecs.public_send(method, args.merge(next_token: page.next_token))
|
116
|
+
results += page.public_send(key)
|
117
|
+
end
|
118
|
+
|
119
|
+
results
|
120
|
+
end
|
121
|
+
|
122
|
+
def ec2_client
|
123
|
+
@ec2_client ||= Aws::EC2::Client.new(
|
124
|
+
region: config.aws.region,
|
125
|
+
credentials: config.aws.credentials
|
126
|
+
)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
data/lib/broadside/utils.rb
CHANGED
data/lib/broadside/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: broadside
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew Leung
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-10-
|
11
|
+
date: 2016-10-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -149,13 +149,14 @@ files:
|
|
149
149
|
- broadside.gemspec
|
150
150
|
- lib/broadside.rb
|
151
151
|
- lib/broadside/configuration.rb
|
152
|
-
- lib/broadside/configuration/
|
153
|
-
- lib/broadside/configuration/
|
152
|
+
- lib/broadside/configuration/aws_config.rb
|
153
|
+
- lib/broadside/configuration/base_config.rb
|
154
|
+
- lib/broadside/configuration/config_struct.rb
|
154
155
|
- lib/broadside/configuration/deploy_config.rb
|
155
156
|
- lib/broadside/configuration/ecs_config.rb
|
156
|
-
- lib/broadside/configuration/struct.rb
|
157
157
|
- lib/broadside/deploy.rb
|
158
158
|
- lib/broadside/deploy/ecs_deploy.rb
|
159
|
+
- lib/broadside/deploy/ecs_manager.rb
|
159
160
|
- lib/broadside/error.rb
|
160
161
|
- lib/broadside/utils.rb
|
161
162
|
- lib/broadside/version.rb
|