ecs_deploy 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4bde853b69a4346173b9ea603cbdbfe7d6597f48
4
+ data.tar.gz: e41e68017918637cf37b93478e48f654fbef1349
5
+ SHA512:
6
+ metadata.gz: b49db5e1393e599ea8f882f4c100c68e503f05e94ff5d1c089f617c4802055fe6c42b83fa894abf5d798cb03bd2d25d9aabee5e86a236b9e945db78b418c85fc
7
+ data.tar.gz: 48364e5d28e5e1b350d3e8cd3951d77ed88146c59bf1456dc18b5e258e74b86b1bdad13589e91c0a1fae0454c7b92c0d9810d42418de9880933bbc341afbfcb8
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ .envrc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ecs_deploy.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # EcsDeploy
2
+
3
+ Helper script for deployment to Amazon ECS.
4
+
5
+ This gem is experimental.
6
+
7
+ Main purpose is combination with capistrano API.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'ecs_deploy', github: "reproio/ecs_deploy"
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ ## Usage
22
+
23
+ Use by Capistrano.
24
+
25
+ ```ruby
26
+ # Capfile
27
+ require 'ecs_deploy/capistrano'
28
+
29
+ # deploy.rb
30
+ set :ecs_default_cluster, "ecs-cluster-name"
31
+ set :ecs_access_key_id, "dummy" # optional, if nil, use environment variable
32
+ set :ecs_secret_access_key, "dummy" # optional, if nil, use environment variable
33
+ set :ecs_region, %w(ap-northeast-1) # optional, if nil, use environment variable
34
+ set :ecs_service_role, "customEcsServiceRole" # default: ecsServiceRole
35
+ set :ecs_deploy_wait_timeout, 600 # default: 300
36
+
37
+ set :ecs_tasks, [
38
+ {
39
+ name: "myapp-#{fetch(:rails_env)}",
40
+ container_definitions: [
41
+ {
42
+ name: "myapp",
43
+ image: "#{fetch(:docker_registry_host_with_port)}/myapp:#{fetch(:sha1)}",
44
+ cpu: 1024,
45
+ memory: 512,
46
+ port_mappings: [],
47
+ essential: true,
48
+ environment: [
49
+ {name: "RAILS_ENV", value: fetch(:rails_env)},
50
+ ],
51
+ mount_points: [
52
+ {
53
+ source_volume: "sockets_path",
54
+ container_path: "/app/tmp/sockets",
55
+ read_only: false,
56
+ },
57
+ ],
58
+ volumes_from: [],
59
+ log_configuration: {
60
+ log_driver: "fluentd",
61
+ options: {
62
+ "tag" => "docker.#{fetch(:rails_env)}.#{name}.{{.ID}}",
63
+ },
64
+ },
65
+ },
66
+ {
67
+ name: "nginx",
68
+ image: "#{fetch(:docker_registry_host_with_port)}/my-nginx",
69
+ cpu: 256,
70
+ memory: 256,
71
+ links: [],
72
+ port_mappings: [
73
+ {container_port: 443, host_port: 443, protocol: "tcp"},
74
+ ],
75
+ essential: true,
76
+ environment: {},
77
+ mount_points: [],
78
+ volumes_from: [
79
+ {source_container: "myapp-#{fetch(:rails_env)}", read_only: false},
80
+ ],
81
+ log_configuration: {
82
+ log_driver: "fluentd",
83
+ options: {
84
+ "tag" => "docker.#{fetch(:rails_env)}.#{name}.{{.ID}}",
85
+ },
86
+ },
87
+ }
88
+ ],
89
+ volumes: [{name: "sockets_path", host: {}}],
90
+ executions: [ # execution task on deploy timing
91
+ {container_overrides: [{name: "myapp", command: ["db_migrate"]}]},
92
+ ]
93
+ },
94
+ ]
95
+
96
+ set :ecs_services, [
97
+ {
98
+ name: "myapp-#{fetch(:rails_env)}",
99
+ elb_name: "service-elb-name",
100
+ elb_service_port: 443,
101
+ elb_healthcheck_port: 443,
102
+ elb_container_name: "nginx",
103
+ desired_count: 1,
104
+ deployment_configuration: {maximum_percent: 200, minimum_healthy_percent: 50},
105
+ },
106
+ ]
107
+ ```
108
+
109
+ ```sh
110
+ cap <stage> ecs:register_task_definition # register ecs_tasks as TaskDefinition
111
+ cap <stage> ecs:deploy # create or update Service by ecs_services info
112
+
113
+ cap <stage> ecs:rollback # deregister current task definition and update Service by previous revision of current task definition
114
+ ```
115
+
116
+ ### Rollback example
117
+
118
+ | sequence | taskdef | service | desc |
119
+ | -------- | -------- | ------------- | ------ |
120
+ | 1 | myapp:12 | myapp-service | |
121
+ | 2 | myapp:13 | myapp-service | |
122
+ | 3 | myapp:14 | myapp-service | current |
123
+
124
+ After rollback
125
+
126
+ | sequence | taskdef | service | desc |
127
+ | -------- | -------- | ------------- | ------ |
128
+ | 1 | myapp:12 | myapp-service | |
129
+ | 2 | myapp:13 | myapp-service | |
130
+ | 3 | myapp:14 | myapp-service | deregister |
131
+ | 4 | myapp:13 | myapp-service | current |
132
+
133
+ And rollback again
134
+
135
+ | sequence | taskdef | service | desc |
136
+ | -------- | -------- | ------------- | ------ |
137
+ | 1 | myapp:12 | myapp-service | |
138
+ | 2 | myapp:13 | myapp-service | previous |
139
+ | 3 | myapp:14 | myapp-service | deregister |
140
+ | 4 | myapp:13 | myapp-service | deregister |
141
+ | 5 | myapp:12 | myapp-service | current |
142
+
143
+ And deploy new version
144
+
145
+ | sequence | taskdef | service | desc |
146
+ | -------- | -------- | ------------- | ------ |
147
+ | 1 | myapp:12 | myapp-service | |
148
+ | 2 | myapp:13 | myapp-service | |
149
+ | 3 | myapp:14 | myapp-service | deregister |
150
+ | 4 | myapp:13 | myapp-service | deregister |
151
+ | 5 | myapp:12 | myapp-service | |
152
+ | 6 | myapp:15 | myapp-service | current |
153
+
154
+ And rollback
155
+
156
+ | sequence | taskdef | service | desc |
157
+ | -------- | -------- | ------------- | ------ |
158
+ | 1 | myapp:12 | myapp-service | |
159
+ | 2 | myapp:13 | myapp-service | |
160
+ | 3 | myapp:14 | myapp-service | deregister |
161
+ | 4 | myapp:13 | myapp-service | deregister |
162
+ | 5 | myapp:12 | myapp-service | |
163
+ | 6 | myapp:15 | myapp-service | deregister |
164
+ | 7 | myapp:12 | myapp-service | current |
165
+
166
+ ## Autoscaler
167
+
168
+ Write config file (YAML format).
169
+
170
+ ```yaml
171
+ # ポーリング時にupscale_triggersに指定した状態のalarmがあればstep分serviceとinstanceを増やす (max_task_countまで)
172
+ # ポーリング時にdownscale_triggersに指定した状態のalarmがあればstep分serviceとinstanceを減らす (min_task_countまで)
173
+ # max_task_countは段階的にリミットを設けられるようにする
174
+ # 一回リミットに到達するとcooldown_for_reach_maxを越えても状態が継続したら再開するようにする
175
+
176
+ polling_interval: 60
177
+
178
+ auto_scaling_groups:
179
+ - name: ecs-cluster-nodes
180
+ region: ap-northeast-1
181
+ buffer: 1 # タスク数に対する余剰のインスタンス数
182
+
183
+ services:
184
+ - name: repro-api-production
185
+ cluster: ecs-cluster
186
+ region: ap-northeast-1
187
+ auto_scaling_group_name: ecs-cluster-nodes
188
+ step: 1
189
+ idle_time: 240
190
+ max_task_count: [10, 25]
191
+ cooldown_time_for_reach_max: 600
192
+ min_task_count: 0
193
+ upscale_triggers:
194
+ - alarm_name: "ECS [repro-api-production] CPUUtilization"
195
+ state: ALARM
196
+ - alarm_name: "ELB repro-api-a HTTPCode_Backend_5XX"
197
+ state: ALARM
198
+ step: 2
199
+ downscale_triggers:
200
+ - alarm_name: "ECS [repro-api-production] CPUUtilization (low)"
201
+ state: OK
202
+ ```
203
+
204
+ ```sh
205
+ ecs_auto_scaler <config yaml>
206
+ ```
207
+
208
+ I recommends deploy `ecs_auto_scaler` on ECS too.
209
+
210
+ ## Development
211
+
212
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
213
+
214
+ 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).
215
+
216
+ ## Contributing
217
+
218
+ Bug reports and pull requests are welcome on GitHub at https://github.com/joker1007/ecs_deploy.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ecs_deploy"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ecs_deploy/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ecs_deploy"
8
+ spec.version = EcsDeploy::VERSION
9
+ spec.authors = ["joker1007"]
10
+ spec.email = ["kakyoin.hierophant@gmail.com"]
11
+
12
+ spec.summary = %q{AWS ECS deploy helper}
13
+ spec.description = %q{AWS ECS deploy helper}
14
+ spec.homepage = "https://github.com/reproio/ecs_deploy"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency "aws-sdk", "~> 2.2"
22
+ spec.add_runtime_dependency "terminal-table"
23
+ spec.add_runtime_dependency "paint"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.11"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ end
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "ecs_deploy"
4
+ require "ecs_deploy/auto_scaler"
5
+
6
+ EcsDeploy::AutoScaler.run(*ARGV)
data/lib/ecs_deploy.rb ADDED
@@ -0,0 +1,28 @@
1
+ require "ecs_deploy/version"
2
+ require "ecs_deploy/configuration"
3
+
4
+ require 'logger'
5
+ require 'terminal-table'
6
+ require 'paint'
7
+
8
+ module EcsDeploy
9
+ def self.logger
10
+ @logger ||= Logger.new(STDOUT).tap do |l|
11
+ l.level = Logger.const_get(config.log_level.to_s.upcase)
12
+ end
13
+ end
14
+
15
+ def self.config
16
+ @config ||= Configuration.new
17
+ end
18
+
19
+ def self.configure(&block)
20
+ if block_given?
21
+ yield config
22
+ @logger = nil
23
+ end
24
+ end
25
+ end
26
+
27
+ require "ecs_deploy/task_definition"
28
+ require "ecs_deploy/service"
@@ -0,0 +1,276 @@
1
+ require 'yaml'
2
+ require 'logger'
3
+ require 'time'
4
+
5
+ module EcsDeploy
6
+ module AutoScaler
7
+ class << self
8
+ attr_reader :logger, :error_logger
9
+
10
+ def run(yaml_path, log_file = nil, error_log_file = nil)
11
+ trap(:TERM) { exit 0 }
12
+ @logger = Logger.new(log_file || STDOUT)
13
+ STDOUT.sync = true unless log_file
14
+ @error_logger = Logger.new(error_log_file || STDERR)
15
+ STDERR.sync = true unless error_log_file
16
+ load_config(yaml_path)
17
+
18
+ until @stop
19
+ run_loop
20
+ end
21
+ end
22
+
23
+ def stop
24
+ @stop = true
25
+ end
26
+
27
+ def run_loop
28
+ service_configs.each do |s|
29
+ next if s.idle?
30
+
31
+ difference = 0
32
+ s.upscale_triggers.each do |trigger|
33
+ step = trigger.step || s.step
34
+ next if difference >= step
35
+
36
+ if trigger.match?
37
+ logger.info "Fire upscale trigger of #{s.name} by #{trigger.alarm_name} #{trigger.state}"
38
+ difference = step
39
+ end
40
+ end
41
+
42
+ if difference == 0 && s.desired_count > s.current_min_task_count
43
+ s.downscale_triggers.each do |trigger|
44
+ if trigger.match?
45
+ logger.info "Fire downscale trigger of #{s.name} by #{trigger.alarm_name} #{trigger.state}"
46
+ step = trigger.step || s.step
47
+ difference = [difference, -(step)].min
48
+ end
49
+ end
50
+ end
51
+
52
+ if s.current_min_task_count > s.desired_count + difference
53
+ difference = s.current_min_task_count - s.desired_count
54
+ end
55
+
56
+ if difference >= 0 && s.desired_count > s.max_task_count.max
57
+ difference = s.max_task_count.max - s.desired_count
58
+ end
59
+
60
+ if difference != 0
61
+ s.update_service(s.desired_count + difference)
62
+ end
63
+ end
64
+
65
+ service_configs.group_by { |s| [s.region, s.auto_scaling_group_name] }.each do |(region, auto_scaling_group_name), configs|
66
+ total_service_count = configs.inject(0) { |sum, s| sum + s.desired_count }
67
+ asg_config = auto_scaling_group_configs.find { |c| c.name == auto_scaling_group_name && c.region == region }
68
+ asg_config.update_auto_scaling_group(total_service_count)
69
+ end
70
+
71
+ TriggerConfig.clear_alarm_cache
72
+
73
+ sleep @polling_interval
74
+ end
75
+
76
+ def load_config(yaml_path)
77
+ @config = YAML.load_file(yaml_path)
78
+ @polling_interval = @config["polling_interval"]
79
+ end
80
+
81
+ def service_configs
82
+ @service_configs ||= @config["services"].map(&ServiceConfig.method(:new))
83
+ end
84
+
85
+ def auto_scaling_group_configs
86
+ @auto_scaling_group_configs ||= @config["auto_scaling_groups"].map(&AutoScalingConfig.method(:new))
87
+ end
88
+ end
89
+
90
+ module ConfigBase
91
+ module ClassMethods
92
+ def client_table
93
+ @client_table ||= {}
94
+ end
95
+ end
96
+
97
+ def initialize(attributes = {})
98
+ attributes.each do |key, val|
99
+ send("#{key}=", val)
100
+ end
101
+ end
102
+ end
103
+
104
+ SERVICE_CONFIG_ATTRIBUTES = %i(name cluster region auto_scaling_group_name step max_task_count min_task_count idle_time scheduled_min_task_count cooldown_time_for_reach_max upscale_triggers downscale_triggers desired_count)
105
+ ServiceConfig = Struct.new(*SERVICE_CONFIG_ATTRIBUTES) do
106
+ include ConfigBase
107
+ extend ConfigBase::ClassMethods
108
+
109
+ def initialize(attributes = {})
110
+ super(attributes)
111
+ self.idle_time ||= 60
112
+ self.max_task_count = Array(max_task_count)
113
+ self.upscale_triggers = upscale_triggers.to_a.map do |t|
114
+ TriggerConfig.new(t.merge(region: region))
115
+ end
116
+ self.downscale_triggers = downscale_triggers.to_a.map do |t|
117
+ TriggerConfig.new(t.merge(region: region))
118
+ end
119
+ self.max_task_count.sort!
120
+ self.desired_count = fetch_service.desired_count
121
+ @reach_max_at = nil
122
+ @last_updated_at = nil
123
+ end
124
+
125
+ def client
126
+ self.class.client_table[region] ||= Aws::ECS::Client.new(
127
+ access_key_id: EcsDeploy.config.access_key_id,
128
+ secret_access_key: EcsDeploy.config.secret_access_key,
129
+ region: region
130
+ )
131
+ end
132
+
133
+ def idle?
134
+ return false unless @last_updated_at
135
+ (Time.now - @last_updated_at) < idle_time
136
+ end
137
+
138
+ def current_min_task_count
139
+ return min_task_count if scheduled_min_task_count.nil? || scheduled_min_task_count.empty?
140
+
141
+ scheduled_min_task_count.find(-> { {"count" => min_task_count} }) { |s|
142
+ from = Time.parse(s["from"])
143
+ to = Time.parse(s["to"])
144
+ (from..to).cover?(Time.now)
145
+ }["count"]
146
+ end
147
+
148
+ def overheat?
149
+ return false unless @reach_max_at
150
+ (Time.now - @reach_max_at) > cooldown_time_for_reach_max
151
+ end
152
+
153
+ def fetch_service
154
+ res = client.describe_services(cluster: cluster, services: [name])
155
+ raise "Service \"#{name}\" is not found" if res.services.empty?
156
+ res.services[0]
157
+ rescue => e
158
+ AutoScaler.error_logger.error(e)
159
+ self.class.client_table[region] = nil
160
+ end
161
+
162
+ def update_service(next_desired_count)
163
+ current_level = max_task_level(desired_count)
164
+ next_level = max_task_level(next_desired_count)
165
+ if current_level < next_level && overheat? # next max
166
+ level = next_level
167
+ @reach_max_at = nil
168
+ AutoScaler.logger.info "Service \"#{name}\" is overheat, uses next max count"
169
+ elsif current_level < next_level && !overheat? # wait cooldown
170
+ level = current_level
171
+ @reach_max_at ||= Time.now
172
+ AutoScaler.logger.info "Service \"#{name}\" waits cooldown in #{(Time.now - @reach_max_at).to_i}sec"
173
+ elsif current_level == next_level && next_desired_count >= max_task_count[current_level] # reach current max
174
+ level = current_level
175
+ @reach_max_at ||= Time.now
176
+ AutoScaler.logger.info "Service \"#{name}\" waits cooldown in #{(Time.now - @reach_max_at).to_i}sec"
177
+ elsif current_level == next_level && next_desired_count < max_task_count[current_level]
178
+ level = current_level
179
+ @reach_max_at = nil
180
+ AutoScaler.logger.info "Service \"#{name}\" clears cooldown state"
181
+ elsif current_level > next_level
182
+ level = next_level
183
+ @reach_max_at = nil
184
+ AutoScaler.logger.info "Service \"#{name}\" clears cooldown state"
185
+ end
186
+
187
+ next_desired_count = [next_desired_count, max_task_count[level]].min
188
+ client.update_service(
189
+ cluster: cluster,
190
+ service: name,
191
+ desired_count: next_desired_count,
192
+ )
193
+ @last_updated_at = Time.now
194
+ self.desired_count = next_desired_count
195
+ AutoScaler.logger.info "Update service \"#{name}\": desired_count -> #{next_desired_count}"
196
+ rescue => e
197
+ AutoScaler.error_logger.error(e)
198
+ self.class.client_table[region] = nil
199
+ end
200
+
201
+ private
202
+
203
+ def max_task_level(count)
204
+ max_task_count.index { |i| count <= i } || max_task_count.size - 1
205
+ end
206
+ end
207
+
208
+ TriggerConfig = Struct.new(:alarm_name, :region, :state, :step) do
209
+ include ConfigBase
210
+ extend ConfigBase::ClassMethods
211
+
212
+ def self.alarm_cache
213
+ @alarm_cache ||= {}
214
+ end
215
+
216
+ def self.clear_alarm_cache
217
+ @alarm_cache.clear if @alarm_cache
218
+ end
219
+
220
+ def client
221
+ self.class.client_table[region] ||= Aws::CloudWatch::Client.new(
222
+ access_key_id: EcsDeploy.config.access_key_id,
223
+ secret_access_key: EcsDeploy.config.secret_access_key,
224
+ region: region
225
+ )
226
+ end
227
+
228
+ def match?
229
+ fetch_alarm.state_value == state
230
+ end
231
+
232
+ def fetch_alarm
233
+ alarm_cache = self.class.alarm_cache
234
+ return alarm_cache[region][alarm_name] if alarm_cache[region] && alarm_cache[region][alarm_name]
235
+
236
+ res = client.describe_alarms(alarm_names: [alarm_name])
237
+ raise "Alarm \"#{alarm_name}\" is not found" if res.metric_alarms.empty?
238
+ res.metric_alarms[0].tap do |alarm|
239
+ AutoScaler.logger.debug(alarm.to_json)
240
+ alarm_cache[region] ||= {}
241
+ alarm_cache[region][alarm_name] = alarm
242
+ end
243
+ rescue => e
244
+ AutoScaler.error_logger.error(e)
245
+ self.class.client_table[region] = nil
246
+ end
247
+ end
248
+
249
+ AutoScalingConfig = Struct.new(:name, :region, :buffer) do
250
+ include ConfigBase
251
+ extend ConfigBase::ClassMethods
252
+
253
+ def client
254
+ self.class.client_table[region] ||= Aws::AutoScaling::Client.new(
255
+ access_key_id: EcsDeploy.config.access_key_id,
256
+ secret_access_key: EcsDeploy.config.secret_access_key,
257
+ region: region
258
+ )
259
+ end
260
+
261
+ def update_auto_scaling_group(total_service_count)
262
+ desired_capacity = total_service_count + buffer.to_i
263
+ client.update_auto_scaling_group(
264
+ auto_scaling_group_name: name,
265
+ min_size: desired_capacity,
266
+ max_size: desired_capacity,
267
+ desired_capacity: desired_capacity,
268
+ )
269
+ AutoScaler.logger.info "Update auto scaling group \"#{name}\": desired_capacity -> #{desired_capacity}"
270
+ rescue => e
271
+ AutoScaler.error_logger.error(e)
272
+ self.class.client_table[region] = nil
273
+ end
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,134 @@
1
+ require 'ecs_deploy'
2
+
3
+ namespace :ecs do
4
+ task :configure do
5
+ EcsDeploy.configure do |c|
6
+ c.log_level = fetch(:ecs_log_level) if fetch(:ecs_log_level)
7
+ c.deploy_wait_timeout = fetch(:ecs_deploy_wait_timeout) if fetch(:ecs_deploy_wait_timeout)
8
+ c.ecs_service_role = fetch(:ecs_service_role) if fetch(:ecs_service_role)
9
+ c.default_region = Array(fetch(:ecs_region))[0] if fetch(:ecs_region)
10
+ end
11
+
12
+ if ENV["TARGET_CLUSTER"]
13
+ set :target_cluster, ENV["TARGET_CLUSTER"].split(",").map(&:strip)
14
+ end
15
+ if ENV["TARGET_TASK_DEFINITION"]
16
+ set :target_task_definition, ENV["TARGET_TASK_DEFINITION"].split(",").map(&:strip)
17
+ end
18
+ end
19
+
20
+ task register_task_definition: [:configure] do
21
+ if fetch(:ecs_tasks)
22
+ regions = Array(fetch(:ecs_region))
23
+ regions = [nil] if regions.empty?
24
+ regions.each do |r|
25
+ fetch(:ecs_tasks).each do |t|
26
+ task_definition = EcsDeploy::TaskDefinition.new(
27
+ region: r,
28
+ task_definition_name: t[:name],
29
+ container_definitions: t[:container_definitions],
30
+ volumes: t[:volumes]
31
+ )
32
+ task_definition.register
33
+
34
+ t[:executions].to_a.each do |exec|
35
+ exec[:cluster] ||= fetch(:ecs_default_cluster)
36
+ task_definition.run(exec)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ task deploy: [:configure, :register_task_definition] do
44
+ if fetch(:ecs_services)
45
+ regions = Array(fetch(:ecs_region))
46
+ regions = [nil] if regions.empty?
47
+ regions.each do |r|
48
+ services = fetch(:ecs_services).map do |service|
49
+ if fetch(:target_cluster) && fetch(:target_cluster).size > 0
50
+ next unless fetch(:target_cluster).include?(service[:cluster])
51
+ end
52
+ if fetch(:target_task_definition) && fetch(:target_task_definition).size > 0
53
+ next unless fetch(:target_task_definition).include?(service[:task_definition_name])
54
+ end
55
+
56
+ service_options = {
57
+ region: r,
58
+ cluster: service[:cluster] || fetch(:ecs_default_cluster),
59
+ service_name: service[:name],
60
+ task_definition_name: service[:task_definition_name],
61
+ elb_name: service[:elb_name],
62
+ elb_service_port: service[:elb_service_port],
63
+ elb_healthcheck_port: service[:elb_healthcheck_port],
64
+ elb_container_name: service[:elb_container_name],
65
+ desired_count: service[:desired_count],
66
+ }
67
+ service_options[:deployment_configuration] = service[:deployment_configuration] if service[:deployment_configuration]
68
+ s = EcsDeploy::Service.new(service_options)
69
+ s.deploy
70
+ s
71
+ end
72
+ EcsDeploy::Service.wait_all_running(services)
73
+ end
74
+ end
75
+ end
76
+
77
+ task rollback: [:configure] do
78
+ if fetch(:ecs_services)
79
+ regions = Array(fetch(:ecs_region))
80
+ regions = [nil] if regions.empty?
81
+ regions.each do |r|
82
+ services = fetch(:ecs_services).map do |service|
83
+ if fetch(:target_cluster) && fetch(:target_cluster).size > 0
84
+ next unless fetch(:target_cluster).include?(service[:cluster])
85
+ end
86
+ if fetch(:target_task_definition) && fetch(:target_task_definition).size > 0
87
+ next unless fetch(:target_task_definition).include?(service[:task_definition_name])
88
+ end
89
+
90
+ task_definition_arns = EcsDeploy::TaskDefinition.new(
91
+ region: r,
92
+ task_definition_name: service[:task_definition_name] || service[:name],
93
+ ).recent_task_definition_arns
94
+
95
+ rollback_step = (ENV["STEP"] || 1).to_i
96
+
97
+ current_task_definition_arn = EcsDeploy::Service.new(
98
+ region: r,
99
+ cluster: service[:cluster] || fetch(:ecs_default_cluster),
100
+ service_name: service[:name],
101
+ ).current_task_definition_arn
102
+
103
+ current_arn_index = task_definition_arns.index do |arn|
104
+ arn == current_task_definition_arn
105
+ end
106
+
107
+ rollback_arn = task_definition_arns[current_arn_index + rollback_step]
108
+
109
+ EcsDeploy.logger.info "#{current_task_definition_arn} -> #{rollback_arn}"
110
+
111
+ raise "Past task_definition_arns is nothing" unless rollback_arn
112
+
113
+ service_options = {
114
+ region: r,
115
+ cluster: service[:cluster] || fetch(:ecs_default_cluster),
116
+ service_name: service[:name],
117
+ task_definition_name: rollback_arn,
118
+ elb_name: service[:elb_name],
119
+ elb_service_port: service[:elb_service_port],
120
+ elb_healthcheck_port: service[:elb_healthcheck_port],
121
+ elb_container_name: service[:elb_container_name],
122
+ desired_count: service[:desired_count],
123
+ }
124
+ service_options[:deployment_configuration] = service[:deployment_configuration] if service[:deployment_configuration]
125
+ s = EcsDeploy::Service.new(service_options)
126
+ s.deploy
127
+ EcsDeploy::TaskDefinition.deregister(current_task_definition_arn, region: r)
128
+ s
129
+ end
130
+ EcsDeploy::Service.wait_all_running(services)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,17 @@
1
+ module EcsDeploy
2
+ class Configuration
3
+ attr_accessor \
4
+ :log_level,
5
+ :access_key_id,
6
+ :secret_access_key,
7
+ :default_region,
8
+ :deploy_wait_timeout,
9
+ :ecs_service_role
10
+
11
+ def initialize
12
+ @log_level = :info
13
+ @deploy_wait_timeout = 300
14
+ @ecs_service_role = "ecsServiceRole"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,101 @@
1
+ require 'timeout'
2
+
3
+ module EcsDeploy
4
+ class Service
5
+ CHECK_INTERVAL = 5
6
+ attr_reader :cluster, :region, :service_name
7
+
8
+ def initialize(
9
+ cluster:, service_name:, task_definition_name: nil, revision: nil,
10
+ elb_name: nil, elb_service_port: nil, elb_healthcheck_port: nil, elb_container_name: nil,
11
+ desired_count: nil, deployment_configuration: {maximum_percent: 200, minimum_healthy_percent: 100},
12
+ region: nil
13
+ )
14
+ @cluster = cluster
15
+ @service_name = service_name
16
+ @task_definition_name = task_definition_name || service_name
17
+ @elb_name = elb_name
18
+ @elb_service_port = elb_service_port
19
+ @elb_healthcheck_port = elb_healthcheck_port
20
+ @elb_container_name = elb_container_name
21
+ @desired_count = desired_count
22
+ @deployment_configuration = deployment_configuration
23
+ @revision = revision
24
+ @region = region || EcsDeploy.config.default_region || ENV["AWS_DEFAULT_REGION"]
25
+ @response = nil
26
+
27
+ @client = Aws::ECS::Client.new(region: @region)
28
+ end
29
+
30
+ def current_task_definition_arn
31
+ res = @client.describe_services(cluster: @cluster, services: [@service_name])
32
+ res.services[0].task_definition
33
+ end
34
+
35
+ def deploy
36
+ res = @client.describe_services(cluster: @cluster, services: [@service_name])
37
+ service_options = {
38
+ cluster: @cluster,
39
+ task_definition: task_definition_name_with_revision,
40
+ deployment_configuration: @deployment_configuration,
41
+ }
42
+ if res.services.empty?
43
+ service_options.merge!({service_name: @service_name})
44
+ if @elb_name
45
+ service_options.merge!({
46
+ role: EcsDeploy.config.ecs_service_role,
47
+ desired_count: @desired_count.to_i,
48
+ load_balancers: [
49
+ {
50
+ load_balancer_name: @elb_name,
51
+ container_name: @elb_container_name,
52
+ container_port: @elb_service_port,
53
+ }
54
+ ],
55
+ })
56
+ end
57
+ @response = @client.create_service(service_options)
58
+ EcsDeploy.logger.info "create service [#{@service_name}] [#{@region}] [#{Paint['OK', :green]}]"
59
+ else
60
+ service_options.merge!({service: @service_name})
61
+ service_options.merge!({desired_count: @desired_count}) if @desired_count
62
+ @response = @client.update_service(service_options)
63
+ EcsDeploy.logger.info "update service [#{@service_name}] [#{@region}] [#{Paint['OK', :green]}]"
64
+ end
65
+ end
66
+
67
+ def wait_running
68
+ return if @response.nil?
69
+
70
+ service = @response.service
71
+ deployment = nil
72
+
73
+ @client.wait_until(:services_stable, cluster: @cluster, services: [service.service_name]) do |w|
74
+ w.delay = 10
75
+
76
+ w.before_attempt do
77
+ EcsDeploy.logger.info "wait service stable [#{service.service_name}]"
78
+ end
79
+ end
80
+ end
81
+
82
+ def self.wait_all_running(services)
83
+ services.group_by { |s| [s.cluster, s.region] }.each do |(cl, region), ss|
84
+ client = Aws::ECS::Client.new(region: region)
85
+ service_names = ss.map(&:service_name)
86
+ client.wait_until(:services_stable, cluster: cl, services: service_names) do |w|
87
+ w.before_attempt do
88
+ EcsDeploy.logger.info "wait service stable [#{service_names.join(", ")}]"
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def task_definition_name_with_revision
97
+ suffix = @revision ? ":#{@revision}" : ""
98
+ "#{@task_definition_name}#{suffix}"
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,88 @@
1
+ module EcsDeploy
2
+ class TaskDefinition
3
+ def self.deregister(arn, region: nil)
4
+ region = region || EcsDeploy.config.default_region || ENV["AWS_DEFAULT_REGION"]
5
+ client = Aws::ECS::Client.new(region: region)
6
+ client.deregister_task_definition({
7
+ task_definition: arn,
8
+ })
9
+ EcsDeploy.logger.info "deregister task definition [#{arn}] [#{region}] [#{Paint['OK', :green]}]"
10
+ end
11
+
12
+ def initialize(
13
+ task_definition_name:, region: nil,
14
+ volumes: [], container_definitions: []
15
+ )
16
+ @task_definition_name = task_definition_name
17
+ @region = region || EcsDeploy.config.default_region || ENV["AWS_DEFAULT_REGION"]
18
+
19
+ @container_definitions = container_definitions.map do |cd|
20
+ if cd[:docker_labels]
21
+ cd[:docker_labels] = cd[:docker_labels].map { |k, v| [k.to_s, v] }.to_h
22
+ end
23
+ if cd[:log_configuration] && cd[:log_configuration][:options]
24
+ cd[:log_configuration][:options] = cd[:log_configuration][:options].map { |k, v| [k.to_s, v] }.to_h
25
+ end
26
+ cd
27
+ end
28
+ @volumes = volumes
29
+
30
+ @client = Aws::ECS::Client.new(region: @region)
31
+ end
32
+
33
+ def recent_task_definition_arns
34
+ resp = @client.list_task_definitions(
35
+ family_prefix: @task_definition_name,
36
+ sort: "DESC"
37
+ )
38
+ resp.task_definition_arns
39
+ rescue
40
+ []
41
+ end
42
+
43
+ def register
44
+ @client.register_task_definition({
45
+ family: @task_definition_name,
46
+ container_definitions: @container_definitions,
47
+ volumes: @volumes,
48
+ })
49
+ EcsDeploy.logger.info "register task definition [#{@task_definition_name}] [#{@region}] [#{Paint['OK', :green]}]"
50
+ end
51
+
52
+ def run(info)
53
+ resp = @client.run_task({
54
+ cluster: info[:cluster],
55
+ task_definition: @task_definition_name,
56
+ overrides: {
57
+ container_overrides: info[:container_overrides] || []
58
+ },
59
+ count: info[:count] || 1,
60
+ started_by: "capistrano",
61
+ })
62
+ unless resp.failures.empty?
63
+ resp.failures.each do |f|
64
+ raise "#{f.arn}: #{f.reason}"
65
+ end
66
+ end
67
+
68
+ wait_targets = Array(info[:wait_stop])
69
+ unless wait_targets.empty?
70
+ @client.wait_until(:tasks_running, cluster: info[:cluster], tasks: resp.tasks.map { |t| t.task_arn })
71
+ @client.wait_until(:tasks_stopped, cluster: info[:cluster], tasks: resp.tasks.map { |t| t.task_arn })
72
+
73
+ resp = @client.describe_tasks(cluster: info[:cluster], tasks: resp.tasks.map { |t| t.task_arn })
74
+ resp.tasks.each do |t|
75
+ t.containers.each do |c|
76
+ next unless wait_targets.include?(c.name)
77
+
78
+ unless c.exit_code.zero?
79
+ raise "Task has errors: #{c.reason}"
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ EcsDeploy.logger.info "run task [#{@task_definition_name} #{info.inspect}] [#{@region}] [#{Paint['OK', :green]}]"
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,3 @@
1
+ module EcsDeploy
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ecs_deploy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - joker1007
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-06-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: terminal-table
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: paint
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.11'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.11'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ description: AWS ECS deploy helper
84
+ email:
85
+ - kakyoin.hierophant@gmail.com
86
+ executables:
87
+ - ecs_auto_scaler
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".gitignore"
92
+ - Gemfile
93
+ - README.md
94
+ - Rakefile
95
+ - bin/console
96
+ - bin/setup
97
+ - ecs_deploy.gemspec
98
+ - exe/ecs_auto_scaler
99
+ - lib/ecs_deploy.rb
100
+ - lib/ecs_deploy/auto_scaler.rb
101
+ - lib/ecs_deploy/capistrano.rb
102
+ - lib/ecs_deploy/configuration.rb
103
+ - lib/ecs_deploy/service.rb
104
+ - lib/ecs_deploy/task_definition.rb
105
+ - lib/ecs_deploy/version.rb
106
+ homepage: https://github.com/reproio/ecs_deploy
107
+ licenses: []
108
+ metadata: {}
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubyforge_project:
125
+ rubygems_version: 2.5.1
126
+ signing_key:
127
+ specification_version: 4
128
+ summary: AWS ECS deploy helper
129
+ test_files: []