ecs_deployer 2.0.0 → 2.1.5

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 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)