ecs_deployer 2.0.0 → 2.1.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7c04f404f3247f1b7908c78f61d4a5139dd7a765
4
- data.tar.gz: cc5867745048b07123f8f556e21c5ebd2f06e394
3
+ metadata.gz: 53c73ee2931e45cc8f3db8a292cef5f467d08895
4
+ data.tar.gz: e5e00d7575c48223a3f6952ba03338b2ed504c7e
5
5
  SHA512:
6
- metadata.gz: 37a8a063edc9c3639a5fe6b8979628c676b44fe8954809da4557441a329fe256ba44e8734dd51cf4e0fc2661612e984ea99e8f2a047273b5c2463c2c80248eeb
7
- data.tar.gz: 8f28ca6748a8a2783af87032ed03194dbe264374f23a37cf77bbf5abef461da15fd11127e81c7cf45144a487573fa7d51351d51528572473324b1681bde05a46
6
+ metadata.gz: f664e88c2e5dbe349f72cf2cbbb683496f9facba1a8b7592944c5c93cf9d0954ee6ea71ac2c540233f959783a16dd56c17e5bd1cfccfe2c85c8cc360d66b3dc0
7
+ data.tar.gz: ba609ad07fb2f6f4a1319fc6ba6592e3d6f18a5a4de0af96ad01216c3f987e322b31d3d28730b8d64280a097edf5e0bb1e23dbbeb9ac094da924d3bb34a0728b
data/.gitignore CHANGED
@@ -7,6 +7,7 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
  /vendor/
10
+ .config.local.yml
10
11
 
11
12
  # rspec failure tracking
12
13
  .rspec_status
data/.rubocop.yml CHANGED
@@ -1,6 +1,9 @@
1
1
  AllCops:
2
2
  Exclude:
3
3
  - 'tmp/**/*'
4
+ - 'vendor/**/*'
5
+ Lint/RescueWithoutErrorClass:
6
+ Enabled: false
4
7
  Style/BlockComments:
5
8
  Enabled: false
6
9
  Style/Documentation:
data/README.md CHANGED
@@ -1,25 +1,21 @@
1
- # EcsDeployer
1
+ # ECS Deployer
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/ecs_deployer.svg)](https://badge.fury.io/rb/ecs_deployer)
4
4
  [![Test Coverage](https://codeclimate.com/github/naomichi-y/ecs_deployer/badges/coverage.svg)](https://codeclimate.com/github/naomichi-y/ecs_deployer/coverage)
5
5
  [![Code Climate](https://codeclimate.com/github/naomichi-y/ecs_deployer/badges/gpa.svg)](https://codeclimate.com/github/naomichi-y/ecs_deployer)
6
6
  [![CircleCI](https://circleci.com/gh/naomichi-y/ecs_deployer/tree/master.svg?style=shield)](https://circleci.com/gh/naomichi-y/ecs_deployer/tree/master)
7
7
 
8
- * [Description](#description)
9
- * [Installation](#installation)
10
- * [Task definition](#task-definition)
11
- * [Encrypt of environment variables](#encrypt-of-environment-variables)
12
- * [Usage](#usage)
13
- * [API](#api)
14
- * [CLI](#cli)
15
- * [Register new task](#register-new-task)
16
- * [Encrypt environment value](#encrypt-environment-value)
17
- * [Decrypt environment value](#decrypt-environment-value)
18
- * [Update service](#update-service)
19
-
20
8
  ## Description
21
9
 
22
- Deploy Docker container on AWS ECS..
10
+ Deploy Docker container on AWS ECS.
11
+
12
+ * Task
13
+ * Create
14
+ * Service
15
+ * Update
16
+ * scheduled task
17
+ * Create
18
+ * Update
23
19
 
24
20
  ## Installation
25
21
 
@@ -47,26 +43,15 @@ Write task definition in YAML format.
47
43
  For available parameters see [Task Definition Parameters](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html).
48
44
 
49
45
  ```yaml
46
+ family: nginx
50
47
  container_definitions:
51
- - name: wordpress
52
- links:
53
- - mysql
54
- image: wordpress
48
+ - name: web
49
+ image: nginx:{{tag}}
55
50
  essential: true
56
51
  port_mappings:
57
52
  - container_port: 80
58
- hostPort: 80
59
- memory: 512
60
- cpu: 10
61
- - environment:
62
- - name: MYSQL_ROOT_PASSWORD
63
- value: password
64
- name: mysql
65
- image: mysql
66
- cpu: 10
67
- memory: 512
68
- essential: true
69
- family: hello_world
53
+ host_port: 80
54
+ memory: 256
70
55
  ```
71
56
 
72
57
  ### Encrypt of environment variables
@@ -86,21 +71,7 @@ Values are decrypted when task is created.
86
71
 
87
72
  ### API
88
73
 
89
- This sample file is in `spec/fixtures/task.yml`.
90
-
91
- ```ruby
92
- deployer = EcsDeployer::Client.new
93
- deployer.register_task('development.yml')
94
- deployer.update_service('cluster', 'development')
95
- ```
96
-
97
- `{{xxx}}` parameter is construed variable.
98
-
99
- ```yaml
100
- container_definitions:
101
- - name: wordpress
102
- image: wordpress:{{tag}}
103
- ```
74
+ Refer to [sample code](https://github.com/naomichi-y/ecs_deployer/tree/master/example).
104
75
 
105
76
  ```ruby
106
77
  deployer.register_task('development.yml', tag: 'latest')
@@ -108,11 +79,13 @@ deployer.register_task('development.yml', tag: 'latest')
108
79
 
109
80
  ### CLI
110
81
 
82
+ Please create `.env` from `.env.default` file, before running.
83
+
111
84
  #### Register new task
112
85
 
113
86
  ```bash
114
87
  $ bundle exec ecs_deployer task-register --path=spec/fixtures/task.yml --replace-variables=tag:latest
115
- Registered task: arn:aws:ecs:ap-northeast-1:xxx:task-definition/hello_world:latest
88
+ Registered task: arn:aws:ecs:ap-northeast-1:xxx:task-definition/nginx:latest
116
89
  ```
117
90
 
118
91
  #### Encrypt environment value
@@ -132,7 +105,7 @@ Decrypted value: xxx
132
105
  #### Update service
133
106
 
134
107
  ```bash
135
- $ bundle exec ecs_deployer update-service --cluster=xxx --service=xxx --wait --timeout=600
108
+ $ bundle exec ecs_deployer update-service --cluster=xxx --service=xxx --wait --wait-timeout=600
136
109
  Start deploying...
137
110
  Deploying... [0/1] (20 seconds elapsed)
138
111
  New task: arn:aws:ecs:ap-northeast-1:xxxx:task-definition/sandbox-development:68
data/config.local.yml ADDED
@@ -0,0 +1,6 @@
1
+ task_path: ~/Projects/nichigas-sandbox/config/deploy/development.yml
2
+ scheduled_task_path: ~/Projects/nichigas-sandbox/config/deploy/development.yml
3
+ cluster: sandbox
4
+ service: development
5
+ scheduled_task_rule: curl
6
+ scheduled_task_target_id: worker
data/config.yml ADDED
@@ -0,0 +1,6 @@
1
+ task_path: ./spec/fixtures/task.yml
2
+ scheduled_task_path: ./spec/fixtures/task.yml
3
+ cluster: default
4
+ service: development
5
+ scheduled_task_rule: curl
6
+ scheduled_task_target_id: worker
data/ecs_deployer.gemspec CHANGED
@@ -1,5 +1,3 @@
1
- # coding: utf-8
2
-
3
1
  lib = File.expand_path('../lib', __FILE__)
4
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
3
  require 'ecs_deployer/version'
@@ -31,15 +29,15 @@ Gem::Specification.new do |spec|
31
29
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
30
  spec.require_paths = ['lib']
33
31
 
32
+ spec.add_dependency 'aws-sdk', '>= 2.9.0'
33
+ spec.add_dependency 'aws_config', '~> 0.1'
34
34
  spec.add_dependency 'oj', '~> 3.0'
35
35
  spec.add_dependency 'thor', '~> 0.19'
36
- spec.add_dependency 'aws-sdk', '~> 2.9'
37
- spec.add_dependency 'aws_config', '~> 0.1'
38
36
  spec.add_development_dependency 'bundler', '~> 1.13'
37
+ spec.add_development_dependency 'codeclimate-test-reporter', '~> 1.0'
38
+ spec.add_development_dependency 'config', '~> 1.5.1'
39
39
  spec.add_development_dependency 'rake', '~> 10.0'
40
40
  spec.add_development_dependency 'rspec', '~> 3.0'
41
- spec.add_development_dependency 'json_spec', '~> 1.1'
42
41
  spec.add_development_dependency 'rubocop', '~> 0.48'
43
42
  spec.add_development_dependency 'simplecov', '~> 0.14'
44
- spec.add_development_dependency 'codeclimate-test-reporter', '~> 1.0'
45
43
  end
@@ -0,0 +1,11 @@
1
+ require 'bundler/setup'
2
+ require 'ecs_deployer'
3
+ require 'config'
4
+
5
+ Config.load_and_set_settings('config.yml', 'config.local.yml')
6
+
7
+ task_path = File.expand_path(Settings.task_path)
8
+ deployer = EcsDeployer::Client.new(Settings.cluster)
9
+ task_definition = deployer.task.register(task_path, tag: 'latest')
10
+
11
+ puts task_definition.task_definition_arn
@@ -0,0 +1,17 @@
1
+ require 'bundler/setup'
2
+ require 'ecs_deployer'
3
+ require 'config'
4
+
5
+ Config.load_and_set_settings('config.yml', 'config.local.yml')
6
+ task_path = File.expand_path(Settings.scheduled_task_path)
7
+
8
+ deployer = EcsDeployer::Client.new(Settings.cluster)
9
+ task_definition = deployer.task.register(task_path, tag: 'latest')
10
+
11
+ scheduled_task = deployer.scheduled_task
12
+ target_builder = scheduled_task.target_builder(Settings.scheduled_task_target_id)
13
+ target_builder.task_definition_arn = task_definition.task_definition_arn
14
+ target_builder.override_container('rails', ['curl', 'http://153.122.13.159/'])
15
+
16
+ task_definition = scheduled_task.update(Settings.scheduled_task_rule, 'cron(* * * * ? *)', [target_builder.to_hash])
17
+ puts task_definition.rule_arn
@@ -0,0 +1,13 @@
1
+ require 'bundler/setup'
2
+ require 'ecs_deployer'
3
+ require 'config'
4
+
5
+ Config.load_and_set_settings('config.yml', 'config.local.yml')
6
+
7
+ task_path = File.expand_path(Settings.task_path)
8
+
9
+ deployer = EcsDeployer::Client.new(Settings.cluster)
10
+ task_definition = deployer.task.register(task_path, tag: 'latest')
11
+ service = deployer.service.update(Settings.service, task_definition)
12
+
13
+ puts service.service_arn
data/lib/ecs_deployer.rb CHANGED
@@ -1,9 +1,15 @@
1
- require 'ecs_deployer/version'
2
1
  require 'ecs_deployer/client'
3
- require 'ecs_deployer/error'
4
2
  require 'ecs_deployer/cli'
3
+ require 'ecs_deployer/error'
4
+ require 'ecs_deployer/service/client'
5
+ require 'ecs_deployer/task/client'
6
+ require 'ecs_deployer/scheduled_task/client'
7
+ require 'ecs_deployer/scheduled_task/target'
8
+ require 'ecs_deployer/util/cipher'
9
+ require 'ecs_deployer/version'
5
10
 
6
11
  module EcsDeployer
12
+ class ClusterNotFoundError < EcsDeployer::Error; end
7
13
  class ServiceNotFoundError < EcsDeployer::Error; end
8
14
  class TaskRunningError < EcsDeployer::Error; end
9
15
  class TaskDefinitionValidateError < EcsDeployer::Error; end
@@ -7,11 +7,9 @@ module EcsDeployer
7
7
 
8
8
  no_commands do
9
9
  def prepare
10
- aws_options = {}
11
- aws_options[:profile] = options[:profile] if options[:profile]
12
- aws_options[:region] = options[:region] if options[:region]
13
-
14
- @deployer = EcsDeployer::Client.new(aws_options)
10
+ @aws_options = {}
11
+ @aws_options[:profile] = options[:profile] if options[:profile]
12
+ @aws_options[:region] = options[:region] if options[:region]
15
13
 
16
14
  nil
17
15
  end
@@ -27,38 +25,39 @@ module EcsDeployer
27
25
  option :replace_variables, type: :hash, default: {}
28
26
  def task_register
29
27
  path = File.expand_path(options[:path], Dir.pwd)
30
- result = @deployer.register_task(path, options[:replace_variables])
28
+ task_client = EcsDeployer::Task::Client.new(@aws_options)
29
+ result = task_client.register(path, options[:replace_variables])
31
30
 
32
- puts "Registered task: #{result}"
31
+ puts "Registered task: #{result.task_definition_arn}"
33
32
  end
34
33
 
35
34
  desc 'update-service', 'Update service difinition.'
36
35
  option :cluster, required: true
37
36
  option :service, required: true
38
37
  option :wait, type: :boolean, default: true
39
- option :timeout, type: :numeric, default: 600
38
+ option :wait_timeout, type: :numeric, default: 600
40
39
  def update_service
41
- @deployer.timeout = options[:timeout]
42
- result = @deployer.update_service(
43
- options[:cluster],
44
- options[:service],
45
- options[:wait]
46
- )
40
+ deploy_client = EcsDeployer::Client.new(options[:cluster], nil, @aws_options)
41
+ service_client = deploy_client.service
42
+ service_client.wait_timeout = options[:wait_timeout]
43
+ result = service_client.update(options[:service], nil, options[:wait])
47
44
 
48
- puts "Update service: #{result}"
45
+ puts "Update service: #{result.service_arn}"
49
46
  end
50
47
 
51
48
  desc 'encrypt', 'Encrypt value of argument with KMS.'
52
49
  option :master_key, required: true
53
50
  option :value, required: true
54
51
  def encrypt
55
- puts "Encrypted value: #{@deployer.encrypt(options[:master_key], options[:value])}"
52
+ cipher = EcsDeployer::Util::Cipher.new(@aws_options)
53
+ puts "Encrypted value: #{cipher.encrypt(options[:master_key], options[:value])}"
56
54
  end
57
55
 
58
56
  desc 'decrypt', 'Decrypt value of argument with KMS.'
59
57
  option :value, required: true
60
58
  def decrypt
61
- puts "Decrypted value: #{@deployer.decrypt(options[:value])}"
59
+ cipher = EcsDeployer::Util::Cipher.new(@aws_options)
60
+ puts "Decrypted value: #{cipher.decrypt(options[:value])}"
62
61
  end
63
62
  end
64
63
  end
@@ -1,262 +1,30 @@
1
- require 'yaml'
2
- require 'oj'
3
- require 'aws-sdk'
4
- require 'base64'
5
1
  require 'logger'
6
2
 
7
3
  module EcsDeployer
8
4
  class Client
9
- LOG_SEPARATOR = '-' * 96
10
- ENCRYPT_PATTERN = /^\${(.+)}$/
11
-
12
- attr_reader :ecs
13
- attr_accessor :wait_timeout, :pauling_interval
14
-
15
5
  # @param [String] cluster
16
6
  # @param [Logger] logger
7
+ # @param [Hash] aws_options
17
8
  # @return [EcsDeployer::Client]
18
9
  def initialize(cluster, logger = nil, aws_options = {})
19
10
  @cluster = cluster
20
- @logger = logger.nil? ? Logger.new(STDOUT) : logger
21
- @ecs = Aws::ECS::Client.new(aws_options)
22
- @kms = Aws::KMS::Client.new(aws_options)
23
- @wait_timeout = 900
24
- @pauling_interval = 20
25
- end
26
-
27
- # @param [String] mater_key
28
- # @param [String] value
29
- # @return [String]
30
- def encrypt(master_key, value)
31
- encode = @kms.encrypt(key_id: "alias/#{master_key}", plaintext: value)
32
- "${#{Base64.strict_encode64(encode.ciphertext_blob)}}"
33
- rescue => e
34
- raise KmsEncryptError, e.to_s
35
- end
36
-
37
- # @param [String] value
38
- # @return [String]
39
- def decrypt(value)
40
- match = value.match(ENCRYPT_PATTERN)
41
- raise KmsDecryptError, 'Encrypted string is invalid.' unless match
42
-
43
- begin
44
- @kms.decrypt(ciphertext_blob: Base64.strict_decode64(match[1])).plaintext
45
- rescue => e
46
- raise KmsDecryptError, e.to_s
47
- end
48
- end
49
-
50
- # @param [String] path
51
- # @param [Hash] replace_variables
52
- # @return [String]
53
- def register_task(path, replace_variables = {})
54
- raise IOError, "File does not exist. [#{path}]" unless File.exist?(path)
55
-
56
- register_task_hash(YAML.load(File.read(path)), replace_variables)
57
- end
58
-
59
- # @param [Hash] task_definition
60
- # @param [Hash] replace_variables
61
- # @return [Aws::ECS::Types::TaskDefinition]
62
- def register_task_hash(task_definition, replace_variables = {})
63
- task_definition = Oj.load(Oj.dump(task_definition), symbol_keys: true)
64
-
65
- replace_parameter_variables!(task_definition, replace_variables)
66
- decrypt_environment_variables!(task_definition)
67
-
68
- result = @ecs.register_task_definition(
69
- container_definitions: task_definition[:container_definitions],
70
- family: task_definition[:family],
71
- task_role_arn: task_definition[:task_role_arn],
72
- volumes: task_definition[:volumes]
73
- )
74
-
75
- result[:task_definition]
11
+ @logger = logger.nil? ? Logger.new(nil) : logger
12
+ @aws_options = aws_options
76
13
  end
77
14
 
78
- # @param [String] service
79
- # @return [String]
80
- def register_clone_task(service)
81
- result = @ecs.describe_services(
82
- cluster: @cluster,
83
- services: [service]
84
- )
85
-
86
- result[:services].each do |svc|
87
- next unless svc[:service_name] == service
88
-
89
- result = @ecs.describe_task_definition(
90
- task_definition: svc[:task_definition]
91
- )
92
-
93
- return register_task_hash(result[:task_definition].to_hash)
94
- end
95
-
96
- raise ServiceNotFoundError, "'#{service}' service is not found."
15
+ # @return [EcsDeployer::Task::Client]
16
+ def task
17
+ EcsDeployer::Task::Client.new(@aws_options)
97
18
  end
98
19
 
99
- # @param [String] service
100
- # @param [Aws::ECS::Types::TaskDefinition] task_definition
101
- # @return [String]
102
- def update_service(service, task_definition = nil, wait = true)
103
- task_definition = register_clone_task(service) if task_definition.nil?
104
- result = @ecs.update_service(
105
- cluster: @cluster,
106
- service: service,
107
- task_definition: task_definition[:family] + ':' + task_definition[:revision].to_s
108
- )
109
-
110
- wait_for_deploy(service, result.service.task_definition) if wait
111
- result.service.service_arn
112
- end
113
-
114
- private
115
-
116
- # @param [Array, Hash] variables
117
- # @param [Hash] replace_variables
118
- def replace_parameter_variables!(variables, replace_variables = {})
119
- for variable in variables do
120
- if variable.class == Array || variable.class == Hash
121
- replace_parameter_variables!(variable, replace_variables)
122
- elsif variable.class == String
123
- replace_variables.each do |replace_key, replace_value|
124
- variable.gsub!("{{#{replace_key}}}", replace_value)
125
- end
126
- end
127
- end
128
- end
129
-
130
- # @param [Hash] task_definition
131
- def decrypt_environment_variables!(task_definition)
132
- raise TaskDefinitionValidateError, '\'container_definition\' is undefined.' unless task_definition.key?(:container_definitions)
133
- task_definition[:container_definitions].each do |container_definition|
134
- next unless container_definition.key?(:environment)
135
-
136
- container_definition[:environment].each do |environment|
137
- if environment[:value].class == String
138
- match = environment[:value].match(ENCRYPT_PATTERN)
139
- environment[:value] = decrypt(match[0]) if match
140
- else
141
- # https://github.com/naomichi-y/ecs_deployer/issues/6
142
- environment[:value] = environment[:value].to_s
143
- end
144
- end
145
- end
20
+ # @return [EcsDeployer::ScheduledTask::Client]
21
+ def scheduled_task
22
+ EcsDeployer::ScheduledTask::Client.new(@cluster, @aws_options)
146
23
  end
147
24
 
148
- # @param [String] service
149
- # @return [Aws::ECS::Types::Service]
150
- def service_status(service)
151
- status = nil
152
- result = @ecs.describe_services(
153
- cluster: @cluster,
154
- services: [service]
155
- )
156
- result[:services].each do |svc|
157
- next unless svc[:service_name] == service
158
- status = svc
159
- break
160
- end
161
-
162
- raise ServiceNotFoundError, "'#{service}' service is not found." if status.nil?
163
-
164
- status
165
- end
166
-
167
- # @param [String] service
168
- # @param [String] task_definition_arn
169
- def detect_stopped_task(service, task_definition_arn)
170
- stopped_tasks = @ecs.list_tasks(
171
- cluster: @cluster,
172
- service_name: service,
173
- desired_status: 'STOPPED'
174
- ).task_arns
175
-
176
- return if stopped_tasks.size.zero?
177
-
178
- description_tasks = @ecs.describe_tasks(
179
- cluster: @cluster,
180
- tasks: stopped_tasks
181
- ).tasks
182
-
183
- description_tasks.each do |task|
184
- raise TaskStoppedError, task.stopped_reason if task.task_definition_arn == task_definition_arn
185
- end
186
- end
187
-
188
- # @param [String] service
189
- # @param [String] task_definition_arn
190
- # @return [Hash]
191
- def deploy_status(service, task_definition_arn)
192
- detect_stopped_task(service, task_definition_arn)
193
-
194
- # Get current tasks
195
- result = @ecs.list_tasks(
196
- cluster: @cluster,
197
- service_name: service,
198
- desired_status: 'RUNNING'
199
- )
200
-
201
- raise TaskRunningError, 'Running task not found.' if result[:task_arns].size.zero?
202
-
203
- result = @ecs.describe_tasks(
204
- cluster: @cluster,
205
- tasks: result[:task_arns]
206
- )
207
-
208
- new_running_count = 0
209
- task_status_logs = []
210
-
211
- result[:tasks].each do |task|
212
- new_running_count += 1 if task_definition_arn == task[:task_definition_arn] && task[:last_status] == 'RUNNING'
213
- task_status_logs << " #{task[:task_definition_arn]} [#{task[:last_status]}]"
214
- end
215
-
216
- {
217
- current_running_count: result[:tasks].size,
218
- new_running_count: new_running_count,
219
- task_status_logs: task_status_logs
220
- }
221
- end
222
-
223
- # @param [String] service
224
- # @param [String] task_definition_arn
225
- def wait_for_deploy(service, task_definition_arn)
226
- service_status = service_status(service)
227
-
228
- wait_time = 0
229
- @logger.info 'Start deploying...'
230
-
231
- loop do
232
- sleep(@pauling_interval)
233
- wait_time += @pauling_interval
234
- result = deploy_status(service, task_definition_arn)
235
-
236
- @logger.info "Deploying... [#{result[:new_running_count]}/#{result[:current_running_count]}] (#{wait_time} seconds elapsed)"
237
- @logger.info "New task: #{task_definition_arn}"
238
- @logger.info LOG_SEPARATOR
239
-
240
- result[:task_status_logs].each do |log|
241
- @logger.info log
242
- end
243
-
244
- @logger.info LOG_SEPARATOR
245
-
246
- if result[:new_running_count] == result[:current_running_count]
247
- @logger.info "Service update succeeded. [#{result[:new_running_count]}/#{result[:current_running_count]}]"
248
- @logger.info "New task definition: #{task_definition_arn}"
249
-
250
- break
251
- else
252
- @logger.info 'You can stop process with Ctrl+C. Deployment will continue.'
253
-
254
- if wait_time > @wait_timeout
255
- @logger.info "New task definition: #{task_definition_arn}"
256
- raise DeployTimeoutError, 'Service is being updating, but process is timed out.'
257
- end
258
- end
259
- end
25
+ # @return [EcsDeployer::Service::Client]
26
+ def service
27
+ EcsDeployer::Service::Client.new(@cluster, @logger, @aws_options)
260
28
  end
261
29
  end
262
30
  end
@@ -0,0 +1,55 @@
1
+ require 'aws-sdk'
2
+
3
+ module EcsDeployer
4
+ module ScheduledTask
5
+ class Client
6
+ # @param [String] cluster
7
+ # @param [Hash] aws_options
8
+ # @return [EcsDeployer::ScheduledTask::Client]
9
+ def initialize(cluster, aws_options = {})
10
+ @cluster = cluster
11
+ @cloud_watch_events = Aws::CloudWatchEvents::Client.new(aws_options)
12
+ @aws_options = aws_options
13
+ end
14
+
15
+ # @param [String] rule
16
+ # @return [Bool]
17
+ def exist_rule?(rule)
18
+ @cloud_watch_events.describe_rule(name: rule)
19
+ true
20
+ rescue Aws::CloudWatchEvents::Errors::ResourceNotFoundException
21
+ false
22
+ end
23
+
24
+ # @param [String] id
25
+ # @param [String] role
26
+ # @return [EcsDeployer::ScheduledTask::Target]
27
+ def target_builder(id, role = 'ecsEventsRole')
28
+ EcsDeployer::ScheduledTask::Target.new(@cluster, id, role, @aws_options)
29
+ end
30
+
31
+ # @param [String] rule
32
+ # @param [String] schedule_expression
33
+ # @param [Array] targets
34
+ # @return [CloudWatchEvents::Types::PutRuleResponse]
35
+ def update(rule, schedule_expression, targets)
36
+ response = @cloud_watch_events.put_rule(
37
+ name: rule,
38
+ schedule_expression: schedule_expression,
39
+ state: 'ENABLED'
40
+ )
41
+ begin
42
+ @cloud_watch_events.put_targets(
43
+ rule: rule,
44
+ targets: targets
45
+ )
46
+
47
+ response
48
+ rescue => e
49
+ @cloud_watch_events.delete_rule(name: rule)
50
+ raise e
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,64 @@
1
+ module EcsDeployer
2
+ module ScheduledTask
3
+ class Target
4
+ attr_reader :id
5
+ attr_accessor :arn, :role_arn, :task_definition_arn, :task_count
6
+
7
+ # @param [String] cluster
8
+ # @param [String] id
9
+ # @param [String] role
10
+ # @param [Hash] aws_options
11
+ # @return EcsDeployer::ScheduledTask::Target]
12
+ def initialize(cluster, id, role = nil, aws_options = {})
13
+ ecs = Aws::ECS::Client.new(aws_options)
14
+ clusters = ecs.describe_clusters(clusters: [cluster]).clusters
15
+ raise ClusterNotFoundError, "Cluster does not eixst. [#{cluster}]" if clusters.count.zero?
16
+
17
+ @id = id
18
+ @arn = clusters[0].cluster_arn
19
+ @role_arn = Aws::IAM::Role.new(role, @aws_options).arn unless role.nil?
20
+ @task_count = 1
21
+ @container_overrides = []
22
+ end
23
+
24
+ # @param [String] name
25
+ # @param [Array] command
26
+ # @param [Hash] environments
27
+ def override_container(name, command = nil, environments = {})
28
+ override_environments = []
29
+ environments.each do |environment|
30
+ environment.each do |env_name, env_value|
31
+ override_environments << {
32
+ name: env_name,
33
+ value: env_value
34
+ }
35
+ end
36
+ end
37
+
38
+ container_override = {
39
+ name: name,
40
+ command: command
41
+ }
42
+ container_overrides[:environment] = override_environments if override_environments.count > 0
43
+
44
+ @container_overrides << container_override
45
+ end
46
+
47
+ # @return [Hash]
48
+ def to_hash
49
+ {
50
+ id: @id,
51
+ arn: @arn,
52
+ role_arn: @role_arn,
53
+ ecs_parameters: {
54
+ task_definition_arn: @task_definition_arn,
55
+ task_count: @task_count
56
+ },
57
+ input: {
58
+ containerOverrides: @container_overrides
59
+ }.to_json.to_s
60
+ }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,155 @@
1
+ require 'aws-sdk'
2
+
3
+ module EcsDeployer
4
+ module Service
5
+ class Client
6
+ LOG_SEPARATOR = '-' * 96
7
+
8
+ attr_accessor :wait_timeout, :polling_interval
9
+
10
+ # @param [String] cluster
11
+ # @param [Logger] logger
12
+ # @param [Hash] aws_options
13
+ # @return [EcsDeployer::Service::Client]
14
+ def initialize(cluster, logger, aws_options = {})
15
+ @cluster = cluster
16
+ @logger = logger
17
+
18
+ @ecs = Aws::ECS::Client.new(aws_options)
19
+ @task = EcsDeployer::Task::Client.new(aws_options)
20
+
21
+ @wait_timeout = 900
22
+ @polling_interval = 20
23
+ end
24
+
25
+ # @param [String] service
26
+ # @param [Aws::ECS::Types::TaskDefinition] task_definition
27
+ # @return [Aws::ECS::Types::Service]
28
+ def update(service, task_definition = nil, wait = true)
29
+ task_definition = @task.register_clone(@cluster, service) if task_definition.nil?
30
+ result = @ecs.update_service(
31
+ cluster: @cluster,
32
+ service: service,
33
+ task_definition: task_definition[:family] + ':' + task_definition[:revision].to_s
34
+ )
35
+
36
+ wait_for_deploy(service, result.service.task_definition) if wait
37
+ result.service
38
+ end
39
+
40
+ private
41
+
42
+ # @param [String] service
43
+ # @return [Bool]
44
+ def exist?(service)
45
+ status = nil
46
+ result = @ecs.describe_services(
47
+ cluster: @cluster,
48
+ services: [service]
49
+ )
50
+ result[:services].each do |svc|
51
+ next unless svc[:service_name] == service
52
+ status = svc
53
+ break
54
+ end
55
+
56
+ status.nil? ? false : true
57
+ end
58
+
59
+ # @param [String] service
60
+ # @param [String] task_definition_arn
61
+ def detect_stopped_task(service, task_definition_arn)
62
+ stopped_tasks = @ecs.list_tasks(
63
+ cluster: @cluster,
64
+ service_name: service,
65
+ desired_status: 'STOPPED'
66
+ ).task_arns
67
+
68
+ return if stopped_tasks.size.zero?
69
+
70
+ description_tasks = @ecs.describe_tasks(
71
+ cluster: @cluster,
72
+ tasks: stopped_tasks
73
+ ).tasks
74
+
75
+ description_tasks.each do |task|
76
+ raise TaskStoppedError, task.stopped_reason if task.task_definition_arn == task_definition_arn
77
+ end
78
+ end
79
+
80
+ # @param [String] service
81
+ # @param [String] task_definition_arn
82
+ # @return [Hash]
83
+ def deploy_status(service, task_definition_arn)
84
+ detect_stopped_task(service, task_definition_arn)
85
+
86
+ # Get current tasks
87
+ result = @ecs.list_tasks(
88
+ cluster: @cluster,
89
+ service_name: service,
90
+ desired_status: 'RUNNING'
91
+ )
92
+
93
+ raise TaskRunningError, 'Running task not found.' if result[:task_arns].size.zero?
94
+
95
+ result = @ecs.describe_tasks(
96
+ cluster: @cluster,
97
+ tasks: result[:task_arns]
98
+ )
99
+
100
+ new_running_count = 0
101
+ task_status_logs = []
102
+
103
+ result[:tasks].each do |task|
104
+ new_running_count += 1 if task_definition_arn == task[:task_definition_arn] && task[:last_status] == 'RUNNING'
105
+ task_status_logs << " #{task[:task_definition_arn]} [#{task[:last_status]}]"
106
+ end
107
+
108
+ {
109
+ current_running_count: result[:tasks].size,
110
+ new_running_count: new_running_count,
111
+ task_status_logs: task_status_logs
112
+ }
113
+ end
114
+
115
+ # @param [String] service
116
+ # @param [String] task_definition_arn
117
+ def wait_for_deploy(service, task_definition_arn)
118
+ raise ServiceNotFoundError, "'#{service}' service is not found." unless exist?(service)
119
+
120
+ wait_time = 0
121
+ @logger.info 'Start deploying...'
122
+
123
+ loop do
124
+ sleep(@polling_interval)
125
+ wait_time += @polling_interval
126
+ result = deploy_status(service, task_definition_arn)
127
+
128
+ @logger.info "Deploying... [#{result[:new_running_count]}/#{result[:current_running_count]}] (#{wait_time} seconds elapsed)"
129
+ @logger.info "New task: #{task_definition_arn}"
130
+ @logger.info LOG_SEPARATOR
131
+
132
+ result[:task_status_logs].each do |log|
133
+ @logger.info log
134
+ end
135
+
136
+ @logger.info LOG_SEPARATOR
137
+
138
+ if result[:new_running_count] == result[:current_running_count]
139
+ @logger.info "Service update succeeded. [#{result[:new_running_count]}/#{result[:current_running_count]}]"
140
+ @logger.info "New task definition: #{task_definition_arn}"
141
+
142
+ break
143
+ else
144
+ @logger.info 'You can stop process with Ctrl+C. Deployment will continue.'
145
+
146
+ if wait_time > @wait_timeout
147
+ @logger.info "New task definition: #{task_definition_arn}"
148
+ raise DeployTimeoutError, 'Service is being updating, but process is timed out.'
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,99 @@
1
+ require 'aws-sdk'
2
+ require 'yaml'
3
+ require 'oj'
4
+
5
+ module EcsDeployer
6
+ module Task
7
+ class Client
8
+ # @param [Hash] aws_options
9
+ # @return [EcsDeployer::Task::Client]
10
+ def initialize(aws_options = {})
11
+ @ecs = Aws::ECS::Client.new(aws_options)
12
+ @cipher = EcsDeployer::Util::Cipher.new(aws_options)
13
+ end
14
+
15
+ # @param [String] path
16
+ # @param [Hash] replace_variables
17
+ # @return [Aws::ECS::Types::TaskDefinition]
18
+ def register(path, replace_variables = {})
19
+ raise IOError, "File does not exist. [#{path}]" unless File.exist?(path)
20
+
21
+ register_hash(YAML.load(File.read(path)), replace_variables)
22
+ end
23
+
24
+ # @param [Hash] task_definition
25
+ # @param [Hash] replace_variables
26
+ # @return [Aws::ECS::Types::TaskDefinition]
27
+ def register_hash(task_definition, replace_variables = {})
28
+ task_definition = Oj.load(Oj.dump(task_definition), symbol_keys: true)
29
+
30
+ replace_parameter_variables!(task_definition, replace_variables)
31
+ decrypt_environment_variables!(task_definition)
32
+
33
+ result = @ecs.register_task_definition(
34
+ container_definitions: task_definition[:container_definitions],
35
+ family: task_definition[:family],
36
+ task_role_arn: task_definition[:task_role_arn],
37
+ volumes: task_definition[:volumes]
38
+ )
39
+
40
+ result[:task_definition]
41
+ end
42
+
43
+ # @param [String] cluster
44
+ # @param [String] service
45
+ # @return [String]
46
+ def register_clone(cluster, service)
47
+ result = @ecs.describe_services(
48
+ cluster: cluster,
49
+ services: [service]
50
+ )
51
+
52
+ result[:services].each do |svc|
53
+ next unless svc[:service_name] == service
54
+
55
+ result = @ecs.describe_task_definition(
56
+ task_definition: svc[:task_definition]
57
+ )
58
+
59
+ return register_hash(result[:task_definition].to_hash)
60
+ end
61
+
62
+ raise ServiceNotFoundError, "'#{service}' service is not found."
63
+ end
64
+
65
+ private
66
+
67
+ # @param [Array, Hash] variables
68
+ # @param [Hash] replace_variables
69
+ def replace_parameter_variables!(variables, replace_variables = {})
70
+ for variable in variables do
71
+ if variable.class == Array || variable.class == Hash
72
+ replace_parameter_variables!(variable, replace_variables)
73
+ elsif variable.class == String
74
+ replace_variables.each do |replace_key, replace_value|
75
+ variable.gsub!("{{#{replace_key}}}", replace_value)
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ # @param [Hash] task_definition
82
+ def decrypt_environment_variables!(task_definition)
83
+ raise TaskDefinitionValidateError, '\'container_definition\' is undefined.' unless task_definition.key?(:container_definitions)
84
+ task_definition[:container_definitions].each do |container_definition|
85
+ next unless container_definition.key?(:environment)
86
+
87
+ container_definition[:environment].each do |environment|
88
+ if environment[:value].class == String
89
+ environment[:value] = @cipher.decrypt(environment[:value]) if @cipher.encrypt_value?(environment[:value])
90
+ else
91
+ # https://github.com/naomichi-y/ecs_deployer/issues/6
92
+ environment[:value] = environment[:value].to_s
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,44 @@
1
+ require 'base64'
2
+
3
+ module EcsDeployer
4
+ module Util
5
+ class Cipher
6
+ ENCRYPT_VARIABLE_PATTERN = /^\${(.+)}$/
7
+
8
+ # @param [Hash] aws_options
9
+ # @return [EcsDeployer::Util::Cipher]
10
+ def initialize(aws_options = {})
11
+ @kms = Aws::KMS::Client.new(aws_options)
12
+ end
13
+
14
+ # @param [String] mater_key
15
+ # @param [String] value
16
+ # @return [String]
17
+ def encrypt(master_key, value)
18
+ encode = @kms.encrypt(key_id: "alias/#{master_key}", plaintext: value)
19
+ "${#{Base64.strict_encode64(encode.ciphertext_blob)}}"
20
+ rescue => e
21
+ raise KmsEncryptError, e.to_s
22
+ end
23
+
24
+ # @param [String] value
25
+ # @return [String]
26
+ def decrypt(value)
27
+ match = value.match(ENCRYPT_VARIABLE_PATTERN)
28
+ raise KmsDecryptError, 'Encrypted string is invalid.' unless match
29
+
30
+ begin
31
+ @kms.decrypt(ciphertext_blob: Base64.strict_decode64(match[1])).plaintext
32
+ rescue => e
33
+ raise KmsDecryptError, e.to_s
34
+ end
35
+ end
36
+
37
+ # @param [String] value
38
+ # @return [Bool]
39
+ def encrypt_value?(value)
40
+ value.to_s.match(ENCRYPT_VARIABLE_PATTERN) ? true : false
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,3 +1,3 @@
1
1
  module EcsDeployer
2
- VERSION = '2.0.0'.freeze
2
+ VERSION = '2.1.5'.freeze
3
3
  end
metadata CHANGED
@@ -1,71 +1,71 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ecs_deployer
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - naomichi-y
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-10-20 00:00:00.000000000 Z
11
+ date: 2017-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: oj
14
+ name: aws-sdk
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '3.0'
19
+ version: 2.9.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '3.0'
26
+ version: 2.9.0
27
27
  - !ruby/object:Gem::Dependency
28
- name: thor
28
+ name: aws_config
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0.19'
33
+ version: '0.1'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0.19'
40
+ version: '0.1'
41
41
  - !ruby/object:Gem::Dependency
42
- name: aws-sdk
42
+ name: oj
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '2.9'
47
+ version: '3.0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '2.9'
54
+ version: '3.0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: aws_config
56
+ name: thor
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0.1'
61
+ version: '0.19'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0.1'
68
+ version: '0.19'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: bundler
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -81,89 +81,89 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '1.13'
83
83
  - !ruby/object:Gem::Dependency
84
- name: rake
84
+ name: codeclimate-test-reporter
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '10.0'
89
+ version: '1.0'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '10.0'
96
+ version: '1.0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: rspec
98
+ name: config
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '3.0'
103
+ version: 1.5.1
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '3.0'
110
+ version: 1.5.1
111
111
  - !ruby/object:Gem::Dependency
112
- name: json_spec
112
+ name: rake
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: '1.1'
117
+ version: '10.0'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: '1.1'
124
+ version: '10.0'
125
125
  - !ruby/object:Gem::Dependency
126
- name: rubocop
126
+ name: rspec
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: '0.48'
131
+ version: '3.0'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: '0.48'
138
+ version: '3.0'
139
139
  - !ruby/object:Gem::Dependency
140
- name: simplecov
140
+ name: rubocop
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
143
  - - "~>"
144
144
  - !ruby/object:Gem::Version
145
- version: '0.14'
145
+ version: '0.48'
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: '0.14'
152
+ version: '0.48'
153
153
  - !ruby/object:Gem::Dependency
154
- name: codeclimate-test-reporter
154
+ name: simplecov
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
157
  - - "~>"
158
158
  - !ruby/object:Gem::Version
159
- version: '1.0'
159
+ version: '0.14'
160
160
  type: :development
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
- version: '1.0'
166
+ version: '0.14'
167
167
  description: Deploy Docker container on AWS ECS.
168
168
  email:
169
169
  - n.yamakita@gmail.com
@@ -182,13 +182,22 @@ files:
182
182
  - bin/console
183
183
  - bin/setup
184
184
  - circle.yml
185
+ - config.local.yml
186
+ - config.yml
185
187
  - ecs_deployer.gemspec
186
- - example/sample.rb
188
+ - example/register_task.rb
189
+ - example/update_scheduled_task.rb
190
+ - example/update_service.rb
187
191
  - exe/ecs_deployer
188
192
  - lib/ecs_deployer.rb
189
193
  - lib/ecs_deployer/cli.rb
190
194
  - lib/ecs_deployer/client.rb
191
195
  - lib/ecs_deployer/error.rb
196
+ - lib/ecs_deployer/scheduled_task/client.rb
197
+ - lib/ecs_deployer/scheduled_task/target.rb
198
+ - lib/ecs_deployer/service/client.rb
199
+ - lib/ecs_deployer/task/client.rb
200
+ - lib/ecs_deployer/util/cipher.rb
192
201
  - lib/ecs_deployer/version.rb
193
202
  homepage: https://github.com/naomichi-y/ecs_deployer
194
203
  licenses:
data/example/sample.rb DELETED
@@ -1,8 +0,0 @@
1
- require 'bundler/setup'
2
- require 'ecs_deployer'
3
-
4
- path = File.expand_path('../spec/fixtures/task.yml', File.dirname(File.realpath(__FILE__)))
5
-
6
- deployer = EcsDeployer::Client.new('sandbox')
7
- task_definition = deployer.register_task(path, tag: 'latest')
8
- deployer.update_service('production', task_definition)