Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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もうすこし勉強したいです
okamu_
no plan inc. CEO 元フリーランスエンジニア/ iOS / サーバーサイド / 共同創業 / 福岡出身 https://qiita.com/organizations/noplan-inc
https://twitter.com/okamu_ro
noplan-inc
no plan株式会社は、Webサイト、iOSアプリ、AndroidアプリなどWebサービス全般の開発から運用をワンストップで行っています。
https://noplan-inc.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away