broadside 1.1.1 → 1.2.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/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
|