ecs_deploy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []