hako 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/README.md +64 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/examples/hello-lb.yml +31 -0
- data/examples/hello.env +1 -0
- data/examples/hello.yml +22 -0
- data/exe/hako +4 -0
- data/hako.gemspec +27 -0
- data/lib/hako.rb +8 -0
- data/lib/hako/cli.rb +18 -0
- data/lib/hako/commander.rb +66 -0
- data/lib/hako/env_expander.rb +91 -0
- data/lib/hako/env_provider.rb +22 -0
- data/lib/hako/env_providers.rb +4 -0
- data/lib/hako/env_providers/file.rb +43 -0
- data/lib/hako/error.rb +4 -0
- data/lib/hako/front.rb +13 -0
- data/lib/hako/front_config.rb +27 -0
- data/lib/hako/fronts.rb +4 -0
- data/lib/hako/fronts/nginx.rb +19 -0
- data/lib/hako/scheduler.rb +36 -0
- data/lib/hako/schedulers.rb +4 -0
- data/lib/hako/schedulers/ecs.rb +310 -0
- data/lib/hako/schedulers/ecs_definition_comparator.rb +51 -0
- data/lib/hako/templates/nginx.conf.erb +13 -0
- data/lib/hako/version.rb +3 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b7b7ad2ffbeb17c94a9e690bf278bb4019852b3e
|
4
|
+
data.tar.gz: 27024dcecc995a1acc893e3072029cd693fd8e0a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 874b5c28b65dd52d728c83a441f23d057807d1f445fe9edc5a5b44200eeb9b8fbcfd9770568a86f2ecee30686dcc389e713a2baed9dfef4e7ade3c05633905f1
|
7
|
+
data.tar.gz: 3218b993bcc4d924f0e6aaaf72a74dc84e2d97f07babcea01295c25625e5fcd13bcb9fad70ea254d30518e42778a14afc17d5df95cec657ddbd7d49f5909748c
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# Hako
|
2
|
+
Deploy Docker container.
|
3
|
+
|
4
|
+
## Status
|
5
|
+
Under development
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'hako'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install hako
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
```
|
26
|
+
% hako deploy examples/hello.yml
|
27
|
+
I, [2015-10-02T12:51:24.530274 #7988] INFO -- : Registered task-definition: arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/hello:29
|
28
|
+
I, [2015-10-02T12:51:24.750501 #7988] INFO -- : Uploaded front configuration to s3://nanika/hako/front_config/hello.conf
|
29
|
+
I, [2015-10-02T12:51:24.877409 #7988] INFO -- : Updated service: arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:service/hello
|
30
|
+
I, [2015-10-02T12:56:07.284874 #7988] INFO -- : Deployment completed
|
31
|
+
|
32
|
+
% hako deploy examples/hello.yml
|
33
|
+
I, [2015-10-02T12:56:12.262760 #8141] INFO -- : Deployment isn't needed
|
34
|
+
|
35
|
+
% hako status examples/hello.yml
|
36
|
+
Load balancer:
|
37
|
+
hako-hello-XXXXXXXXXX.ap-northeast-1.elb.amazonaws.com:80 -> front:80
|
38
|
+
Deployments:
|
39
|
+
[PRIMARY] desired_count=2, pending_count=0, running_count=2
|
40
|
+
Tasks:
|
41
|
+
[RUNNING]: i-XXXXXXXX (ecs-001)
|
42
|
+
[RUNNING]: i-YYYYYYYY (ecs-002)
|
43
|
+
Events:
|
44
|
+
2015-10-05 13:35:53 +0900: (service hello) has reached a steady state.
|
45
|
+
2015-10-05 13:35:14 +0900: (service hello) stopped 1 running tasks.
|
46
|
+
|
47
|
+
```
|
48
|
+
|
49
|
+
## Front image
|
50
|
+
The front container receives these environment variables.
|
51
|
+
|
52
|
+
- `S3_CONFIG_BUCKET` and `S3_CONFIG_KEY`
|
53
|
+
- The front container should download configuration file from S3.
|
54
|
+
|
55
|
+
## Development
|
56
|
+
|
57
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
58
|
+
|
59
|
+
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).
|
60
|
+
|
61
|
+
## Contributing
|
62
|
+
|
63
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/eagletmt/hako.
|
64
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "hako"
|
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,31 @@
|
|
1
|
+
image: ryotarai/hello-sinatra
|
2
|
+
env:
|
3
|
+
$providers:
|
4
|
+
- type: file
|
5
|
+
path: examples/hello.env
|
6
|
+
PORT: 3000
|
7
|
+
MESSAGE: '#{username}-san'
|
8
|
+
port: 3000
|
9
|
+
scheduler:
|
10
|
+
type: ecs
|
11
|
+
region: ap-northeast-1
|
12
|
+
memory: 100
|
13
|
+
cpu: 100
|
14
|
+
cluster: eagletmt
|
15
|
+
desired_count: 2
|
16
|
+
role: ecsServiceRole
|
17
|
+
elb:
|
18
|
+
listeners:
|
19
|
+
- load_balancer_port: 80
|
20
|
+
subnets:
|
21
|
+
- subnet-XXXXXXXX
|
22
|
+
- subnet-YYYYYYYY
|
23
|
+
security_groups:
|
24
|
+
- sg-ZZZZZZZZ
|
25
|
+
front:
|
26
|
+
type: nginx
|
27
|
+
image_tag: hako-nginx
|
28
|
+
s3:
|
29
|
+
region: ap-northeast-1
|
30
|
+
bucket: nanika
|
31
|
+
prefix: hako/front_config
|
data/examples/hello.env
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
username=eagletmt
|
data/examples/hello.yml
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
image: ryotarai/hello-sinatra
|
2
|
+
env:
|
3
|
+
$providers:
|
4
|
+
- type: file
|
5
|
+
path: examples/hello.env
|
6
|
+
PORT: 3000
|
7
|
+
MESSAGE: '#{username}-san'
|
8
|
+
port: 3000
|
9
|
+
scheduler:
|
10
|
+
type: ecs
|
11
|
+
region: ap-northeast-1
|
12
|
+
memory: 100
|
13
|
+
cpu: 100
|
14
|
+
cluster: eagletmt
|
15
|
+
desired_count: 2
|
16
|
+
front:
|
17
|
+
type: nginx
|
18
|
+
image_tag: hako-nginx
|
19
|
+
s3:
|
20
|
+
region: ap-northeast-1
|
21
|
+
bucket: nanika
|
22
|
+
prefix: hako/front_config
|
data/exe/hako
ADDED
data/hako.gemspec
ADDED
@@ -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 'hako/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "hako"
|
8
|
+
spec.version = Hako::VERSION
|
9
|
+
spec.authors = ["Kohei Suzuki"]
|
10
|
+
spec.email = ["eagletmt@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Deploy Docker container}
|
13
|
+
spec.description = %q{Deploy Docker container}
|
14
|
+
spec.homepage = "https://github.com/eagletmt/hako"
|
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_dependency "aws-sdk", "~> 2.1.0"
|
22
|
+
spec.add_dependency "thor"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler"
|
25
|
+
spec.add_development_dependency "rake"
|
26
|
+
spec.add_development_dependency "rspec"
|
27
|
+
end
|
data/lib/hako.rb
ADDED
data/lib/hako/cli.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module Hako
|
4
|
+
class CLI < Thor
|
5
|
+
desc 'deploy FILE', 'Run deployment'
|
6
|
+
option :force, aliases: %w[-f], type: :boolean, default: false, desc: 'Run deployment even if nothing is changed'
|
7
|
+
def deploy(yaml_path)
|
8
|
+
require 'hako/commander'
|
9
|
+
Commander.new(yaml_path).deploy(force: options[:force])
|
10
|
+
end
|
11
|
+
|
12
|
+
desc 'status FILE', 'Show deployment status'
|
13
|
+
def status(yaml_path)
|
14
|
+
require 'hako/commander'
|
15
|
+
Commander.new(yaml_path).status
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'hako/env_expander'
|
3
|
+
require 'hako/error'
|
4
|
+
require 'hako/front_config'
|
5
|
+
require 'hako/fronts'
|
6
|
+
require 'hako/schedulers'
|
7
|
+
|
8
|
+
module Hako
|
9
|
+
class Commander
|
10
|
+
PROVIDERS_KEY = '$providers'
|
11
|
+
|
12
|
+
def initialize(yaml_path)
|
13
|
+
@app_id = Pathname.new(yaml_path).basename.sub_ext('').to_s
|
14
|
+
@yaml = YAML.load_file(yaml_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def deploy(force: false)
|
18
|
+
env = @yaml['env'].dup
|
19
|
+
providers = load_providers(env.delete(PROVIDERS_KEY) || [])
|
20
|
+
env = EnvExpander.new(providers).expand(env)
|
21
|
+
|
22
|
+
front = load_front(@yaml['front'])
|
23
|
+
|
24
|
+
scheduler = load_scheduler(@yaml['scheduler'])
|
25
|
+
app_port = @yaml.fetch('port', nil)
|
26
|
+
image_tag = @yaml['image'] # TODO: Append revision
|
27
|
+
scheduler.deploy(image_tag, env, app_port, front, force: force)
|
28
|
+
end
|
29
|
+
|
30
|
+
def status
|
31
|
+
load_scheduler(@yaml['scheduler']).status
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def load_providers(provider_configs)
|
37
|
+
provider_configs.map do |config|
|
38
|
+
type = config['type']
|
39
|
+
unless type
|
40
|
+
raise Error.new("type must be set in each #{PROVIDERS_KEY} element")
|
41
|
+
end
|
42
|
+
require "hako/env_providers/#{type}"
|
43
|
+
Hako::EnvProviders.const_get(camelize(type)).new(config)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def load_scheduler(scheduler_config)
|
48
|
+
type = scheduler_config['type']
|
49
|
+
unless type
|
50
|
+
raise Error.new('type must be set in scheduler')
|
51
|
+
end
|
52
|
+
require "hako/schedulers/#{type}"
|
53
|
+
Hako::Schedulers.const_get(camelize(type)).new(@app_id, scheduler_config)
|
54
|
+
end
|
55
|
+
|
56
|
+
def load_front(yaml)
|
57
|
+
front_config = FrontConfig.new(yaml)
|
58
|
+
require "hako/fronts/#{front_config.type}"
|
59
|
+
Hako::Fronts.const_get(camelize(front_config.type)).new(front_config)
|
60
|
+
end
|
61
|
+
|
62
|
+
def camelize(name)
|
63
|
+
name.split('_').map(&:capitalize).join('')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'strscan'
|
3
|
+
require 'hako/env_providers'
|
4
|
+
require 'hako/error'
|
5
|
+
|
6
|
+
module Hako
|
7
|
+
class EnvExpander
|
8
|
+
class ExpansionError < Error
|
9
|
+
end
|
10
|
+
|
11
|
+
class Literal < Struct.new(:literal)
|
12
|
+
end
|
13
|
+
|
14
|
+
class Variable < Struct.new(:name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(providers)
|
18
|
+
@providers = providers
|
19
|
+
end
|
20
|
+
|
21
|
+
def expand(env)
|
22
|
+
parsed_env = {}
|
23
|
+
variables = Set.new
|
24
|
+
env.each do |key, val|
|
25
|
+
tokens = parse(val.to_s)
|
26
|
+
tokens.each do |t|
|
27
|
+
if t.is_a?(Variable)
|
28
|
+
variables << t.name
|
29
|
+
end
|
30
|
+
end
|
31
|
+
parsed_env[key] = tokens
|
32
|
+
end
|
33
|
+
|
34
|
+
values = {}
|
35
|
+
@providers.each do |provider|
|
36
|
+
if variables.empty?
|
37
|
+
break
|
38
|
+
end
|
39
|
+
provider.ask(variables.to_a).each do |var, val|
|
40
|
+
values[var] = val
|
41
|
+
variables.delete(var)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
unless variables.empty?
|
45
|
+
raise ExpansionError.new("Unresolvable variables: #{variables.to_a}")
|
46
|
+
end
|
47
|
+
|
48
|
+
expanded_env = {}
|
49
|
+
parsed_env.each do |key, tokens|
|
50
|
+
expanded_env[key] = tokens.map { |t| expand_value(values, t) }.join('')
|
51
|
+
end
|
52
|
+
expanded_env
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def parse(value)
|
58
|
+
s = StringScanner.new(value)
|
59
|
+
tokens = []
|
60
|
+
pos = 0
|
61
|
+
while s.scan_until(/#\{(.*?)\}/)
|
62
|
+
pre = s.string.byteslice(pos ... (s.pos - s.matched.size))
|
63
|
+
var = s[1]
|
64
|
+
unless pre.empty?
|
65
|
+
tokens << Literal.new(pre)
|
66
|
+
end
|
67
|
+
if var.empty?
|
68
|
+
raise ExpansionError.new("Empty interpolation is not allowed")
|
69
|
+
else
|
70
|
+
tokens << Variable.new(var)
|
71
|
+
end
|
72
|
+
pos = s.pos
|
73
|
+
end
|
74
|
+
unless s.rest.empty?
|
75
|
+
tokens << Literal.new(s.rest)
|
76
|
+
end
|
77
|
+
tokens
|
78
|
+
end
|
79
|
+
|
80
|
+
def expand_value(values, token)
|
81
|
+
case token
|
82
|
+
when Literal
|
83
|
+
token.literal
|
84
|
+
when Variable
|
85
|
+
values.fetch(token.name)
|
86
|
+
else
|
87
|
+
raise ExpansionError.new("Unknown token type: #{token.class}")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'hako/error'
|
2
|
+
|
3
|
+
module Hako
|
4
|
+
class EnvProvider
|
5
|
+
class ValidationError < Error
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(_options)
|
9
|
+
raise NotImplementedError
|
10
|
+
end
|
11
|
+
|
12
|
+
def ask(_variables)
|
13
|
+
raise NotImplementedError
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def validation_error!(message)
|
19
|
+
raise ValidationError.new(message)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'hako/env_provider'
|
2
|
+
|
3
|
+
module Hako
|
4
|
+
module EnvProviders
|
5
|
+
class File < EnvProvider
|
6
|
+
def initialize(options)
|
7
|
+
unless options['path']
|
8
|
+
validation_error!("path must be set")
|
9
|
+
end
|
10
|
+
@path = options['path']
|
11
|
+
end
|
12
|
+
|
13
|
+
def ask(variables)
|
14
|
+
env = {}
|
15
|
+
read_from_file do |key, val|
|
16
|
+
if variables.include?(key)
|
17
|
+
env[key] = val
|
18
|
+
end
|
19
|
+
end
|
20
|
+
env
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def read_from_file(&block)
|
26
|
+
::File.open(@path) do |f|
|
27
|
+
f.each_line do |line|
|
28
|
+
line.chomp!
|
29
|
+
line.lstrip!
|
30
|
+
if line[0] == '#'
|
31
|
+
# line comment
|
32
|
+
next
|
33
|
+
end
|
34
|
+
key, val = line.split('=', 2)
|
35
|
+
if val
|
36
|
+
block.call(key, val)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/hako/error.rb
ADDED
data/lib/hako/front.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module Hako
|
4
|
+
class FrontConfig < Struct.new(:type, :image_tag, :s3)
|
5
|
+
class S3Config < Struct.new(:region, :bucket, :prefix)
|
6
|
+
def initialize(options)
|
7
|
+
self.region = options.fetch('region')
|
8
|
+
self.bucket = options.fetch('bucket')
|
9
|
+
self.prefix = options.fetch('prefix', nil)
|
10
|
+
end
|
11
|
+
|
12
|
+
def key(app_id)
|
13
|
+
if prefix
|
14
|
+
"#{prefix}/#{app_id}.conf"
|
15
|
+
else
|
16
|
+
"#{app_id}.conf"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(options)
|
22
|
+
self.type = options.fetch('type')
|
23
|
+
self.image_tag = options.fetch('image_tag')
|
24
|
+
self.s3 = S3Config.new(options.fetch('s3'))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/hako/fronts.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'hako/front'
|
3
|
+
|
4
|
+
module Hako
|
5
|
+
module Fronts
|
6
|
+
class Nginx < Front
|
7
|
+
def generate_config(app_port)
|
8
|
+
listen_spec = "app:#{app_port}"
|
9
|
+
ERB.new(File.read(nginx_conf_erb), nil, '-').result(binding)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def nginx_conf_erb
|
15
|
+
File.expand_path('../../templates/nginx.conf.erb', __FILE__)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
module Hako
|
4
|
+
class Scheduler
|
5
|
+
class ValidationError < Error
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(_app_id, _options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def deploy(_image_tag, _env, _app_port, _front_config, _options)
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def status
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def upload_front_config(app_id, front, app_port)
|
20
|
+
front_conf = front.generate_config(app_port)
|
21
|
+
s3_config = front.config.s3
|
22
|
+
s3 = Aws::S3::Client.new(region: s3_config.region)
|
23
|
+
s3.put_object(
|
24
|
+
body: front_conf,
|
25
|
+
bucket: s3_config.bucket,
|
26
|
+
key: s3_config.key(app_id),
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def validation_error!(message)
|
33
|
+
raise ValidationError.new(message)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,310 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require 'hako'
|
3
|
+
require 'hako/error'
|
4
|
+
require 'hako/scheduler'
|
5
|
+
require 'hako/schedulers/ecs_definition_comparator'
|
6
|
+
|
7
|
+
module Hako
|
8
|
+
module Schedulers
|
9
|
+
class Ecs < Scheduler
|
10
|
+
DEFAULT_CLUSTER = 'default'
|
11
|
+
DEFAULT_FRONT_PORT = 10000
|
12
|
+
|
13
|
+
def initialize(app_id, options)
|
14
|
+
@app_id = app_id
|
15
|
+
@cluster = options.fetch('cluster', DEFAULT_CLUSTER)
|
16
|
+
@desired_count = options.fetch('desired_count') { validation_error!('desired_count must be set') }
|
17
|
+
@cpu = options.fetch('cpu') { validation_error!('cpu must be set') }
|
18
|
+
@memory = options.fetch('memory') { validation_error!('memory must be set') }
|
19
|
+
region = options.fetch('region') { validation_error!('region must be set') }
|
20
|
+
@role = options.fetch('role', nil)
|
21
|
+
@ecs = Aws::ECS::Client.new(region: region)
|
22
|
+
@elb = Aws::ElasticLoadBalancing::Client.new(region: region)
|
23
|
+
@ec2 = Aws::EC2::Client.new(region: region)
|
24
|
+
@elb_config = options.fetch('elb', nil)
|
25
|
+
end
|
26
|
+
|
27
|
+
def deploy(image_tag, env, app_port, front, force: false)
|
28
|
+
@force_mode = force
|
29
|
+
front_env = {
|
30
|
+
'AWS_DEFAULT_REGION' => front.config.s3.region,
|
31
|
+
'S3_CONFIG_BUCKET' => front.config.s3.bucket,
|
32
|
+
'S3_CONFIG_KEY' => front.config.s3.key(@app_id),
|
33
|
+
}
|
34
|
+
front_port = determine_front_port(front)
|
35
|
+
task_definition = register_task_definition(image_tag, env, front.config, front_env, front_port)
|
36
|
+
if task_definition == :noop
|
37
|
+
Hako.logger.info "Task definition isn't changed"
|
38
|
+
task_definition = @ecs.describe_task_definition(task_definition: @app_id).task_definition
|
39
|
+
else
|
40
|
+
Hako.logger.info "Registered task definition: #{task_definition.task_definition_arn}"
|
41
|
+
upload_front_config(@app_id, front, app_port)
|
42
|
+
Hako.logger.info "Uploaded front configuration to s3://#{front.config.s3.bucket}/#{front.config.s3.key(@app_id)}"
|
43
|
+
end
|
44
|
+
service = create_or_update_service(task_definition.task_definition_arn, front_port)
|
45
|
+
if service == :noop
|
46
|
+
Hako.logger.info "Service isn't changed"
|
47
|
+
else
|
48
|
+
Hako.logger.info "Updated service: #{service.service_arn}"
|
49
|
+
wait_for_ready(service)
|
50
|
+
end
|
51
|
+
Hako.logger.info "Deployment completed"
|
52
|
+
end
|
53
|
+
|
54
|
+
def status
|
55
|
+
service = @ecs.describe_services(cluster: @cluster, services: [@app_id]).services[0]
|
56
|
+
unless service
|
57
|
+
puts 'Unavailable'
|
58
|
+
exit 1
|
59
|
+
end
|
60
|
+
|
61
|
+
unless service.load_balancers.empty?
|
62
|
+
lb = service.load_balancers[0]
|
63
|
+
lb_detail = @elb.describe_load_balancers(load_balancer_names: [lb.load_balancer_name]).load_balancer_descriptions[0]
|
64
|
+
puts 'Load balancer:'
|
65
|
+
lb_detail.listener_descriptions.each do |ld|
|
66
|
+
l = ld.listener
|
67
|
+
puts " #{lb_detail.dns_name}:#{l.load_balancer_port} -> #{lb.container_name}:#{lb.container_port}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
puts 'Deployments:'
|
72
|
+
service.deployments.each do |d|
|
73
|
+
puts " [#{d.status}] desired_count=#{d.desired_count}, pending_count=#{d.pending_count}, running_count=#{d.running_count}"
|
74
|
+
end
|
75
|
+
|
76
|
+
puts 'Tasks:'
|
77
|
+
@ecs.list_tasks(cluster: @cluster, service_name: @app_id).each do |page|
|
78
|
+
unless page.task_arns.empty?
|
79
|
+
tasks = @ecs.describe_tasks(cluster: @cluster, tasks: page.task_arns).tasks
|
80
|
+
container_instances = {}
|
81
|
+
@ecs.describe_container_instances(cluster: @cluster, container_instances: tasks.map(&:container_instance_arn)).container_instances.each do |ci|
|
82
|
+
container_instances[ci.container_instance_arn] = ci
|
83
|
+
end
|
84
|
+
ec2_instances = {}
|
85
|
+
@ec2.describe_instances(instance_ids: container_instances.values.map(&:ec2_instance_id)).reservations.each do |r|
|
86
|
+
r.instances.each do |i|
|
87
|
+
ec2_instances[i.instance_id] = i
|
88
|
+
end
|
89
|
+
end
|
90
|
+
tasks.each do |task|
|
91
|
+
ci = container_instances[task.container_instance_arn]
|
92
|
+
instance = ec2_instances[ci.ec2_instance_id]
|
93
|
+
print " [#{task.last_status}]: #{ci.ec2_instance_id}"
|
94
|
+
if instance
|
95
|
+
name_tag = instance.tags.find { |t| t.key == 'Name' }
|
96
|
+
if name_tag
|
97
|
+
print " (#{name_tag.value})"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
puts
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
puts 'Events:'
|
106
|
+
service.events.first(10).each do |e|
|
107
|
+
puts " #{e.created_at}: #{e.message}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def determine_front_port(front)
|
114
|
+
service = @ecs.describe_services(cluster: @cluster, services: [@app_id]).services[0]
|
115
|
+
if service
|
116
|
+
find_front_port(service)
|
117
|
+
else
|
118
|
+
max_port = -1
|
119
|
+
@ecs.list_services(cluster: @cluster).each do |page|
|
120
|
+
unless page.service_arns.empty?
|
121
|
+
@ecs.describe_services(cluster: @cluster, services: page.service_arns).services.each do |service|
|
122
|
+
max_port = [max_port, find_front_port(service)].max
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
if max_port == -1
|
127
|
+
DEFAULT_FRONT_PORT
|
128
|
+
else
|
129
|
+
max_port+1
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def find_front_port(service)
|
135
|
+
task_definition = @ecs.describe_task_definition(task_definition: service.task_definition).task_definition
|
136
|
+
container_definitions = {}
|
137
|
+
task_definition.container_definitions.each do |c|
|
138
|
+
container_definitions[c.name] = c
|
139
|
+
end
|
140
|
+
container_definitions['front'].port_mappings[0].host_port
|
141
|
+
end
|
142
|
+
|
143
|
+
def task_definition_changed?(front, app)
|
144
|
+
if @force_mode
|
145
|
+
return true
|
146
|
+
end
|
147
|
+
task_definition = @ecs.describe_task_definition(task_definition: @app_id).task_definition
|
148
|
+
container_definitions = {}
|
149
|
+
task_definition.container_definitions.each do |c|
|
150
|
+
container_definitions[c.name] = c
|
151
|
+
end
|
152
|
+
different_definition?(front, container_definitions['front']) || different_definition?(app, container_definitions['app'])
|
153
|
+
rescue Aws::ECS::Errors::ClientException
|
154
|
+
# Task definition does not exist
|
155
|
+
true
|
156
|
+
end
|
157
|
+
|
158
|
+
def different_definition?(expected_container, actual_container)
|
159
|
+
EcsDefinitionComparator.new(expected_container).different?(actual_container)
|
160
|
+
end
|
161
|
+
|
162
|
+
def register_task_definition(image_tag, env, front_config, front_env, front_port)
|
163
|
+
front = front_container(front_config, front_env, front_port)
|
164
|
+
app = app_container(image_tag, env)
|
165
|
+
if task_definition_changed?(front, app)
|
166
|
+
@ecs.register_task_definition(
|
167
|
+
family: @app_id,
|
168
|
+
container_definitions: [front, app],
|
169
|
+
).task_definition
|
170
|
+
else
|
171
|
+
:noop
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def front_container(front_config, env, front_port)
|
176
|
+
environment = env.map { |k, v| { name: k, value: v } }
|
177
|
+
{
|
178
|
+
name: 'front',
|
179
|
+
image: front_config.image_tag,
|
180
|
+
cpu: 100,
|
181
|
+
memory: 100,
|
182
|
+
links: ['app:app'],
|
183
|
+
port_mappings: [{container_port: 80, host_port: front_port, protocol: 'tcp'}],
|
184
|
+
essential: true,
|
185
|
+
environment: environment,
|
186
|
+
}
|
187
|
+
end
|
188
|
+
|
189
|
+
def app_container(image_tag, env)
|
190
|
+
environment = env.map { |k, v| { name: k, value: v } }
|
191
|
+
{
|
192
|
+
name: 'app',
|
193
|
+
image: image_tag,
|
194
|
+
cpu: @cpu,
|
195
|
+
memory: @memory,
|
196
|
+
links: [],
|
197
|
+
port_mappings: [],
|
198
|
+
essential: true,
|
199
|
+
environment: environment,
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
def create_or_update_service(task_definition_arn, front_port)
|
204
|
+
services = @ecs.describe_services(cluster: @cluster, services: [@app_id]).services
|
205
|
+
if services.empty?
|
206
|
+
params = {
|
207
|
+
cluster: @cluster,
|
208
|
+
service_name: @app_id,
|
209
|
+
task_definition: task_definition_arn,
|
210
|
+
desired_count: @desired_count,
|
211
|
+
role: @role,
|
212
|
+
}
|
213
|
+
if @elb_config
|
214
|
+
name = find_or_create_load_balancer(front_port)
|
215
|
+
params.merge!(
|
216
|
+
load_balancers: [
|
217
|
+
{
|
218
|
+
load_balancer_name: name,
|
219
|
+
container_name: 'front',
|
220
|
+
container_port: 80,
|
221
|
+
},
|
222
|
+
],
|
223
|
+
)
|
224
|
+
end
|
225
|
+
@ecs.create_service(params).service
|
226
|
+
else
|
227
|
+
service = services[0]
|
228
|
+
if service.status != 'ACTIVE'
|
229
|
+
raise Error.new("Service #{service.service_arn} is already exist but the status is #{service.status}")
|
230
|
+
end
|
231
|
+
params = {
|
232
|
+
cluster: @cluster,
|
233
|
+
service: @app_id,
|
234
|
+
desired_count: @desired_count,
|
235
|
+
task_definition: task_definition_arn,
|
236
|
+
}
|
237
|
+
if service_changed?(service, params)
|
238
|
+
@ecs.update_service(params).service
|
239
|
+
else
|
240
|
+
:noop
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
SERVICE_KEYS = %i[desired_count task_definition]
|
246
|
+
|
247
|
+
def service_changed?(service, params)
|
248
|
+
SERVICE_KEYS.each do |key|
|
249
|
+
if service.public_send(key) != params[key]
|
250
|
+
return true
|
251
|
+
end
|
252
|
+
end
|
253
|
+
false
|
254
|
+
end
|
255
|
+
|
256
|
+
def wait_for_ready(service)
|
257
|
+
latest_event_id = service.events[0].id
|
258
|
+
loop do
|
259
|
+
s = @ecs.describe_services(cluster: service.cluster_arn, services: [service.service_arn]).services[0]
|
260
|
+
s.events.each do |e|
|
261
|
+
if e.id == latest_event_id
|
262
|
+
break
|
263
|
+
end
|
264
|
+
Hako.logger.info "#{e.created_at}: #{e.message}"
|
265
|
+
end
|
266
|
+
latest_event_id = s.events[0].id
|
267
|
+
finished = s.deployments.all? { |d| d.status != 'ACTIVE' }
|
268
|
+
if finished
|
269
|
+
return
|
270
|
+
else
|
271
|
+
sleep 1
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def find_or_create_load_balancer(front_port)
|
277
|
+
unless load_balancer_exist?(elb_name)
|
278
|
+
listeners = @elb_config.fetch('listeners').map do |l|
|
279
|
+
{
|
280
|
+
protocol: 'tcp',
|
281
|
+
load_balancer_port: l.fetch('load_balancer_port'),
|
282
|
+
instance_port: front_port,
|
283
|
+
ssl_certificate_id: l.fetch('ssl_certificate_id', nil),
|
284
|
+
}
|
285
|
+
end
|
286
|
+
lb = @elb.create_load_balancer(
|
287
|
+
load_balancer_name: elb_name,
|
288
|
+
listeners: listeners,
|
289
|
+
subnets: @elb_config.fetch('subnets'),
|
290
|
+
security_groups: @elb_config.fetch('security_groups'),
|
291
|
+
tags: @elb_config.fetch('tags', {}).map { |k, v| { key: k, value: v.to_s } },
|
292
|
+
)
|
293
|
+
Hako.logger.info "Created ELB #{lb.dns_name} with instance_port=#{front_port}"
|
294
|
+
end
|
295
|
+
elb_name
|
296
|
+
end
|
297
|
+
|
298
|
+
def load_balancer_exist?(name)
|
299
|
+
@elb.describe_load_balancers(load_balancer_names: [elb_name])
|
300
|
+
true
|
301
|
+
rescue Aws::ElasticLoadBalancing::Errors::LoadBalancerNotFound
|
302
|
+
false
|
303
|
+
end
|
304
|
+
|
305
|
+
def elb_name
|
306
|
+
"hako-#{@app_id}"
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Hako
|
2
|
+
module Schedulers
|
3
|
+
class EcsDefinitionComparator
|
4
|
+
def initialize(expected_container)
|
5
|
+
@expected_container = expected_container
|
6
|
+
end
|
7
|
+
|
8
|
+
CONTAINER_KEYS = %i[image cpu memory links]
|
9
|
+
PORT_MAPPING_KEYS = %i[container_port host_port protocol]
|
10
|
+
ENVIRONMENT_KEYS = %i[name value]
|
11
|
+
|
12
|
+
def different?(actual_container)
|
13
|
+
unless actual_container
|
14
|
+
return true
|
15
|
+
end
|
16
|
+
if different_members?(@expected_container, actual_container, CONTAINER_KEYS)
|
17
|
+
return true
|
18
|
+
end
|
19
|
+
if @expected_container[:port_mappings].size != actual_container.port_mappings.size
|
20
|
+
return true
|
21
|
+
end
|
22
|
+
@expected_container[:port_mappings].zip(actual_container.port_mappings) do |e, a|
|
23
|
+
if different_members?(e, a, PORT_MAPPING_KEYS)
|
24
|
+
return true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
if @expected_container[:environment].size != actual_container.environment.size
|
28
|
+
return true
|
29
|
+
end
|
30
|
+
@expected_container[:environment].zip(actual_container.environment) do |e, a|
|
31
|
+
if different_members?(e, a, ENVIRONMENT_KEYS)
|
32
|
+
return true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
false
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def different_members?(expected, actual, keys)
|
42
|
+
keys.each do |key|
|
43
|
+
if actual.public_send(key) != expected[key]
|
44
|
+
return true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
server {
|
2
|
+
listen 80;
|
3
|
+
|
4
|
+
location / {
|
5
|
+
proxy_pass http://<%= listen_spec %>;
|
6
|
+
proxy_set_header Host $host;
|
7
|
+
proxy_set_header Connection ""; # for upstream keepalive
|
8
|
+
proxy_http_version 1.1; # for upstream keepalive
|
9
|
+
proxy_connect_timeout 5s;
|
10
|
+
proxy_send_timeout 20s;
|
11
|
+
proxy_read_timeout 20s;
|
12
|
+
}
|
13
|
+
}
|
data/lib/hako/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hako
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kohei Suzuki
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-10-05 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.1.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.1.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: thor
|
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: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
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: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Deploy Docker container
|
84
|
+
email:
|
85
|
+
- eagletmt@gmail.com
|
86
|
+
executables:
|
87
|
+
- hako
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- ".rspec"
|
93
|
+
- ".travis.yml"
|
94
|
+
- Gemfile
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- bin/console
|
98
|
+
- bin/setup
|
99
|
+
- examples/hello-lb.yml
|
100
|
+
- examples/hello.env
|
101
|
+
- examples/hello.yml
|
102
|
+
- exe/hako
|
103
|
+
- hako.gemspec
|
104
|
+
- lib/hako.rb
|
105
|
+
- lib/hako/cli.rb
|
106
|
+
- lib/hako/commander.rb
|
107
|
+
- lib/hako/env_expander.rb
|
108
|
+
- lib/hako/env_provider.rb
|
109
|
+
- lib/hako/env_providers.rb
|
110
|
+
- lib/hako/env_providers/file.rb
|
111
|
+
- lib/hako/error.rb
|
112
|
+
- lib/hako/front.rb
|
113
|
+
- lib/hako/front_config.rb
|
114
|
+
- lib/hako/fronts.rb
|
115
|
+
- lib/hako/fronts/nginx.rb
|
116
|
+
- lib/hako/scheduler.rb
|
117
|
+
- lib/hako/schedulers.rb
|
118
|
+
- lib/hako/schedulers/ecs.rb
|
119
|
+
- lib/hako/schedulers/ecs_definition_comparator.rb
|
120
|
+
- lib/hako/templates/nginx.conf.erb
|
121
|
+
- lib/hako/version.rb
|
122
|
+
homepage: https://github.com/eagletmt/hako
|
123
|
+
licenses: []
|
124
|
+
metadata: {}
|
125
|
+
post_install_message:
|
126
|
+
rdoc_options: []
|
127
|
+
require_paths:
|
128
|
+
- lib
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
requirements: []
|
140
|
+
rubyforge_project:
|
141
|
+
rubygems_version: 2.4.5.1
|
142
|
+
signing_key:
|
143
|
+
specification_version: 4
|
144
|
+
summary: Deploy Docker container
|
145
|
+
test_files: []
|