hako 0.1.0

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