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.
@@ -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
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ spec/examples.txt
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.3
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in hako.gemspec
4
+ gemspec
@@ -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
+
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -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
@@ -0,0 +1 @@
1
+ username=eagletmt
@@ -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
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'hako/cli'
3
+
4
+ Hako::CLI.start
@@ -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
@@ -0,0 +1,8 @@
1
+ require 'logger'
2
+ require "hako/version"
3
+
4
+ module Hako
5
+ def self.logger
6
+ @logger ||= Logger.new($stdout)
7
+ end
8
+ end
@@ -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,4 @@
1
+ module Hako
2
+ module EnvProviders
3
+ end
4
+ 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
@@ -0,0 +1,4 @@
1
+ module Hako
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,13 @@
1
+ module Hako
2
+ class Front
3
+ attr_reader :config
4
+
5
+ def initialize(front_config)
6
+ @config = front_config
7
+ end
8
+
9
+ def generate_config(_app_port)
10
+ raise NotImplementedError
11
+ end
12
+ end
13
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ module Hako
2
+ module Fronts
3
+ end
4
+ end
@@ -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,4 @@
1
+ module Hako
2
+ module Schedulers
3
+ end
4
+ 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
+ }
@@ -0,0 +1,3 @@
1
+ module Hako
2
+ VERSION = "0.1.0"
3
+ end
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: []