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