capistrano
haproxy
docker
インフラ
docker-compose

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

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

普段dockerをデプロイするときには、ecrにpushしたり最近流行りのRancherだとcliを叩いたり、
それをcircleciに頼んだりしていたのですが、
今回は訳あってcapistranoを使うことになったので、そのことについて書きます

capistranoとは

  • Capistrano 3はRubyをベースにしたサーバ操作およびデプロイの自動化ツールです。Capistrano 3を利用することで、デプロイなどの複雑なサーバ操作を自動化することができます。
  • 要するにコマンド1発でデプロイできます
  • capistranoの基本的な設定はこちらなどを参考にしてください

やっていること

  1. 開いているポートを探す
  2. 新しいコンテナを立ち上げる
  3. 古いコンテナを止める
  4. 古いコンテナを削除する
  • もしも途中で失敗したら、ロールバックして元に戻す

構成

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

Untitled.001.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もうすこし勉強したいです