Capistranoでdocker-compose.ymlをデプロイする

# Capistranoでdocker-compose.ymlをデプロイする
普段dockerをデプロイするときには、ecrにpushしたり最近流行りの[Rancher](http://qiita.com/okamu_/items/da52b4dc12221feca55c)だとcliを叩いたり、
それをcircleciに頼んだりしていたのですが、
今回は訳あってcapistranoを使うことになったので、そのことについて書きます

## capistranoとは
- Capistrano 3はRubyをベースにしたサーバ操作およびデプロイの自動化ツールです。Capistrano 3を利用することで、デプロイなどの複雑なサーバ操作を自動化することができます。
- **要するにコマンド１発でデプロイできます**
- capistranoの基本的な設定は[こちら](http://labs.gree.jp/blog/2013/12/10084/)などを参考にしてください

## やっていること
0. 開いているポートを探す
1. 新しいコンテナを立ち上げる
2. 古いコンテナを止める
3. 古いコンテナを削除する

- もしも途中で失敗したら、ロールバックして元に戻す

## 構成
- 今回は、7777ポートと7778ポートを交互にデプロイします。その際HAProxyでcheckしているので、ほぼ無停止でデプロイできます。

![Untitled.001.jpeg](https://qiita-image-store.s3.amazonaws.com/0/62822/cbe6a003-c801-24c0-b400-5aed195b45cc.jpeg)

#### それでは実際のコードですが

## docker-compose.yml
- CAP_DOCKER_COMPOSE_PORTでdocker-composeのポートを指定しています

```docker-compose.yml
version: '2'
services:
  nginx:
    build: ./nginx
    ports:
      - "${CAP_DOCKER_COMPOSE_PORT}:80"
    command: sh -c "nginx -g 'daemon off;'"
    volumes: 
      - run:/var/run/
    restart: always
    logging:
      driver: "awslogs"
      options:
        awslogs-region: "ap-northeast-1"
        awslogs-group: "${NODE_ENV}"
        awslogs-stream: "nginx"
  node:
    build: ./node
    volumes:
      - run:/var/run/
    command: sh -c "sh run.sh" 
    restart: always
    logging:
      driver: "awslogs"
      options:
        awslogs-region: "ap-northeast-1"
        awslogs-group: "${NODE_ENV}"
        awslogs-stream: "node"
volumes:
  run:


```

## deploy.rb
```config/deploy.rb
set :application, 'app-name' 
set :repo_url, 'git@github.com:hoghoghoho.git'
set :branch, 'master'

set :containers_log, deploy_path + 'containers.log'
set :pty, false
set :linked_dirs, %w{log}

set :docker_compose_port_range, 7777..7778

set :keep_releases, 3

set :ssh_options, {
  keys: [File.expand_path('./ssh/id_rsa')],
  forward_agent: true,
  auth_methods: %w(publickey)
}

Rake::Task['metrics:collect'].clear_actions

namespace :deploy do
  ## deploy
  task :before_path_setting do
    on roles(fetch(:docker_compose_roles)) do
      set :rollback_path, previous_release
    end
  end
# コンテナをスタートさせます
  task :start_containers do
    on roles(fetch(:docker_compose_roles)) do
      set :previous_release_path, previous_release
      within release_path do
        with cap_docker_compose_root_path: fetch(:deploy_to), cap_docker_compose_port: fetch(:release_port) do
          execute :'docker-compose', '-f', "docker-compose.yml", 'up','-d'
          sleep 3
        end
      end
    end
  end

# 古いコンテナを削除します
  task :purge_old_containers do
    on roles(fetch(:docker_compose_roles)) do
      if fetch(:previous_release_path, false)
        within fetch(:previous_release_path) do
          info "Purging containers of previous release at #{fetch(:previous_release_path)}"
          with cap_docker_compose_port: fetch(:release_port) do
           execute :'docker-compose', 'stop'
          end
        end
      end
    end
  end
# 途中でデプロイに失敗したら、新しいコンテナを破棄して、元に戻します
  task :purge_failed_containers do
    set :cap_docker_compose_failed, true
    on roles(fetch(:docker_compose_roles)) do
      if fetch(:previous_release_path, false)
        within release_path do
          with cap_docker_compose_port: fetch(:release_port) do
            info "Purging failed containers at #{release_path}"
            execute :'docker-compose', 'down'
          end
        end
      end
    end
  end

# 古いコンテナをstopします
  task :stop_previous_release do
    on roles(fetch(:docker_compose_roles)) do
      set :previous_release_path, previous_release
      if fetch(:previous_release_path, false)
        within fetch(:previous_release_path) do
        containers = capture :'docker', 'ps', '-q'
          unless containers.empty?
            info "Stopping containers of previous release"
            execute :"export CAP_DOCKER_COMPOSE_PORT=#{fetch(:release_port)} && cd #{previous_release} && docker-compose down"
          end
        end
      end
    end
  end

# 空いているポートを探す
  def detect_available_port
    ports = fetch(:docker_compose_port_range)
    ports.each do |port|
      port_response = capture("netstat -lnt | awk '$6 == \"LISTEN\" && $4 ~ \".#{port}\"'")
      if port_response.empty?
        info "Port #{port} of #{ports.to_s} is free"
        return port
      end
    end
    raise "No port available in range #{ports.to_s}. Deployment aborted."
  end


  def previous_release
    path = "#{fetch(:deploy_to)}/current"
    if test("[ -L #{path} ]")
      return capture("readlink -f #{path}")
    end
    return nil
  end

  #==================================================
  # rollback
  #==================================================

  task :start_rollback_containers do
    on roles(fetch(:docker_compose_roles)) do
      set :previous_release_path, previous_release
      set :release_port, detect_available_port
      within release_path do
        with cap_docker_compose_root_path: fetch(:deploy_to), cap_docker_compose_port: fetch(:release_port) do
          execute :"export CAP_DOCKER_COMPOSE_PORT=#{fetch(:release_port)}&& cd #{previous_release} && RAILS_ENV=#{fetch(:rails_env)} docker-compose -f docker-compose.yml up -d"
          sleep 3
        end
      end
    end
  end

  task :stop_current_release do
    on roles(fetch(:docker_compose_roles)) do
      if fetch(:rollback_path, false)
        within fetch(:rollback_path) do
          info "Stopping containers of previous release"
          execute :"export CAP_DOCKER_COMPOSE_PORT=8888#{fetch(:release_port)} && cd #{previous_release} && docker-compose down"
        end
      end
    end
  end

  desc 'reset task'
  task :reset do
    on roles(fetch(:docker_compose_roles)) do
      containers = capture :'docker', 'ps', '-q'
      unless containers.empty?
        execute :'docker stop `docker ps -q`'
        execute :'docker rm `docker ps -q -a`'
        #execute :'docker network ls -q | xargs docker network rm'
      end
      invoke 'deploy'
    end
  end

  after :updating, :start_containers
  before :updated, :stop_previous_release
  after :failed, :purge_failed_containers
  after :failed, :cleanup_rollback
  after :finished, :purge_old_containers unless fetch(:cap_docker_compose_failed, false)

  after :rollback, :start_rollback_containers 
  before :reverting, :stop_current_release
  before :starting, :before_path_setting

end

namespace :load do
  task :defaults do
    set :docker_compose_roles, fetch(:docker_compose_roles, :all)
  end
end


```

## haproxy.cfg
```haproxy.cfg
global
	log /dev/log	local0
	log /dev/log	local1 notice
	chroot /var/lib/haproxy
	user haproxy
	group haproxy
	daemon

defaults
	log	global
	mode	http
	option	httplog
	option	dontlognull
	option http-server-close
	option redispatch
  timeout connect 5000
	timeout http-keep-alive 15s
  timeout server 50000
  timeout client 50000
	errorfile 400 /etc/haproxy/errors/400.http
	errorfile 403 /etc/haproxy/errors/403.http
	errorfile 408 /etc/haproxy/errors/408.http
	errorfile 500 /etc/haproxy/errors/500.http
	errorfile 502 /etc/haproxy/errors/502.http
	errorfile 503 /etc/haproxy/errors/503.http
	errorfile 504 /etc/haproxy/errors/504.http

lesten server
	bind *:3000
	mode tcp

	balance roundrobin
	option forwardfor
	option httpchk get /
	http-check expect ! rstatus ^5
	default-server fall 1 rise 1
	server container1 127.0.0.1:7777 observe layer7
	server container2 127.0.0.1:7778 observe layer7
```
- `bind *:3000ポートで待ち受けています`
- `observe layer7` この部分で生存チェックをしています

## デプロイする

```
$ bundle exec cap production deploy
```
- これでしばらく待っていると、デプロイされます
- 試しに、serverで`docker ps`など打つと良いでしょう

## まとめ
- ロールバックの実装が大変だったです
- haproxyもうすこし勉強したいです