ブログ

ryuzeeによるブログ記事。不定期更新

Docker + Capistrano3で簡単にWebアプリをデプロイする

こんにちは。@ryuzeeです。

アプリケーションのデプロイを楽にするためにDockerを使いたいけど、別にクラスタは必要ない規模だったりクラスタの管理もしたくないという人は多いのではないかと思います。 そこで、今回は、DockerとCapistrano3を組み合わせて単にデプロイを楽にする方法を紹介します。

構成図

まず今回の構成図はこんな感じです。AWS上での構成例になっていますが別にどの環境でもあまり関係ない普通のWebアプリケーションを想定してください。

実現したい要件

次に実現する要件です。特に変わったことはありません。

  • いつも同じ方式でデプロイする
  • ダウンタイムなしでデプロイする
  • デプロイに失敗したら簡単にロールバックできるようにする
  • サーバが増えてもデプロイの方式は変えなくて済むようにする
  • サーバを再起動してもサービスは自動で復旧する

方式

では方式を見ていきましょう。

WebアプリケーションのDockerイメージ化

今回は、Dockerを使うので、WebアプリケーションをDockerイメージ化します。今回の構成では、複数コンテナ間の連携はないので簡単です。 なお、デプロイにおけるベストプラクティスとして、開発環境やステージング環境でテストした成果物(Artifacts)を本番に反映するときに何らかの変更をするべきではない、というものがあります。 したがってデータベースへの接続情報などをはじめとする環境固有の情報は全てコンテナ起動時に引数(環境変数)を使って渡せるようにしてください(言わずもがなですが)。

また、イメージを作成する際は、必ずイメージに何らかのバージョン情報をタグとして付与するようにします。latestタグしかないとデプロイするイメージを指定したりロールバックするのが困難になります。 僕の場合は、Railsアプリケーションにバージョン情報を持たせ、イメージをビルドする際にそのバージョンを反映するようにしました。以下は、それを行なうRakefileの例です(Build Onceを実現しつつ、タグを複数つけようとしているので若干複雑にはなっています)。

# -*- coding: utf-8 -*-
require "#{File.dirname(__FILE__)}/lib/oss/version"
require 'open3'

task :default => :build

task :build do
  cmd = "docker build -q -t ryuzee/slidehub:latest . 2>/dev/null | awk '/Successfully built/{print $NF}'"
  o, e, _s = Open3.capture3(cmd)
  if o.chomp! == '' || e != ''
    raise 'Failed to build Docker image...'
  end

  cmd = "docker tag -f #{o} ryuzee/slidehub:#{SlideHub::VERSION}"
  o, e, _s = Open3.capture3(cmd)
  if o.chomp! == '' || e != ''
    raise 'Failed to add version tag to Docker image...'
  end
end

task :push do
  cmd = 'docker push ryuzee/slidehub:latest'
  sh cmd
  cmd = "docker push ryuzee/slidehub:#{SlideHub::VERSION}"
  sh cmd
end

Capistrano3を使って外部からコンテナを起動し切り替える

さて、単にサーバでコンテナを起動するだけであれば、Capistrano3で、単にdocker runコマンドを実行すればOKです。 たとえば、Capistrano3のタスクは以下のような感じになります。おおよその処理の流れとしては以下になります(ただしこのやり方には問題があるので後述)。

  • 必要なイメージをpullしてくる
  • –envオプションを使ってコンテナを起動する
  • コンテナが起動しても、コンテナの中のプロセス(Rails)がすぐに起動してくるとは限らない(特に遅いサーバ)し、エラーになる可能性もあるので、curlを使って指定回数HTTPのステータスをチェックする
  • 一定回数の間にステータスコード200が返ってくればOKそうなので、Nginxのproxy_passの設定を書き換えて、Nginxをリロードする
  • 最後にコンテナが一杯動いていると問題なので、今起動したものとその前に起動していたものを残して強制的に削除する
  # 前略
  desc 'deploy'
  task :deploy do
    on roles(:container) do
      execute 'sudo docker pull ryuzee/slidehub:latest'
      prefix = DateTime.now.strftime('%Y%m%d%H%M%s')
      cmd = <<"EOS"
        docker run -d \
        --env OSS_DB_NAME=#{fetch(:oss_db_name)} \
        --env OSS_DB_USERNAME=#{fetch(:oss_db_username)} \
        --env OSS_DB_PASSWORD=#{fetch(:oss_db_password)} \
        --env OSS_DB_URL=#{fetch(:oss_db_url)} \
        -P --name slidehub#{prefix} ryuzee/slidehub
EOS
      container_id = capture("sudo #{cmd}")
      port = capture("sudo docker port #{container_id} 3000").to_s.split(':')[1]
      puts port
      # confirm running
      cmd = "curl -LI http://127.0.0.1:#{port} -o /dev/null -w '%{http_code}\\n' -s"
      cnt = 0
      loop do
        execute "echo 'sleep 20 sec...'"
        sleep 20
        cnt += 1
        if cnt == 10
          error = Exception.new('An error that should abort and rollback deployment')
          raise error
        end
        begin
          container_status = capture(cmd).to_i
          if container_status == 200
            break
          end
        rescue Exception => e
          puts e.inspect
        end
      end
      data = { port: port }
      template 'nginx_default.erb', '/tmp/default', data, true
      execute 'sudo mv /tmp/default /etc/nginx/sites-available/default && sudo service nginx reload'

      containers = capture('sudo docker ps -q').to_s.split("\n")
      containers.shift
      containers.shift
      puts containers.inspect
      containers.each do |c|
        execute "sudo docker rm -f #{c}"
      end
    end
  end
end

なお、この方式には問題があって、サーバが再起動してしまった時の考慮や新しくサーバを増やしたときの考慮ができていません。次はそれを検討しましょう。

再起動などにも対応するために起動処理をホスト側に移す

上の例には問題があるのは説明したとおりで、再起動した場合や新たにサーバを追加した場合を考慮しましょう。 そうすると、以下のような方式に変わります。

  • そのコンテナを動かすサーバにはあらかじめ環境変数を設定しておく。Ubuntuであれば/etc/environmentあたりに設定する。もしくはetcdとかを使っても良い(が小規模環境だと面倒)。
  • AWSを使っているならAmazon EC2の起動時にUser-data経由で環境変数を設定してもOK
  • コンテナを動かすためのスクリプトをサーバ側にインストールしておく。これをサーバの起動時に動かしたり、デプロイ時にCapistrano3経由でキックする。

これを踏まえて以下のような実装になります。

コンテナを動かすためのスクリプト

  • 今回はbashで作っているが別にPerlでもPythonでもなんでもホスト側に入っている言語ならOK
  • コンテナを動かすホスト側に配置する。場所はどこでも良い(たとえば、/usr/local/bin/run_app.sh)
  • このスクリプト自体の配置をCapistrano3のセットアップタスクで配置してもOK。
  • スクリプトでは引数にコンテナのタグを指定できるようにしておくとロールバックしやすい
#!/bin/bash

. /etc/environment

TAG='latest'
if [ -z $1 ]; then
  /usr/bin/docker pull ryuzee/slidehub:$TAG
else
  TAG=$1
  /usr/bin/docker pull ryuzee/slidehub:$TAG
  if [ $? -ne 0 ]; then
    TAG='latest'
    /usr/bin/docker pull ryuzee/slidehub:$TAG
  fi
fi

PREFIX=`date +%Y%m%d%H%M%s`

CONTAINER_ID=`/usr/bin/docker run -d \
        --env OSS_DB_NAME=$OSS_DB_NAME \
        --env OSS_DB_USERNAME=$OSS_DB_USERNAME \
        --env OSS_DB_PASSWORD=$OSS_DB_PASSWORD \
        --env OSS_DB_URL=$OSS_DB_URL \
-P --name slidehub$PREFIX ryuzee/slidehub:$TAG`

echo "Container ID is $CONTAINER_ID"

PORT_STR=`docker port $CONTAINER_ID 3000`
IFS=':'
set -- $PORT_STR
PORT=$2
echo "Listening port is $PORT"

CNT=0
while :
do
  echo 'Confirming the container is listening...'
  CNT=`expr $CNT + 1`
  if [ $CNT -gt 20 ]
  then
    echo 'Failed to launch container...'
    exit 1
  fi
  STATUS_CODE=`curl -LI http://127.0.0.1:${PORT} -o /dev/null -w '%{http_code}\\n' -s`
  echo $STATUS_CODE
  if [ $STATUS_CODE == 200 ]
  then
    break
  fi
  sleep 20
done

sed -i -e "s/proxy_pass http:\/\/127\.0\.0\.1:[0-9]*;/proxy_pass http:\/\/127.0.0.1:${PORT};/g" /etc/nginx/sites-enabled/default
service nginx reload

IFS=$'\n'
CONTAINERS=(`sudo docker ps -qa --filter "name=slidehub[0-9]{22,}"`)
n=0
for line in "${CONTAINERS[@]}"; do
  n=`expr $n + 1`
  if test $n -gt 2 ; then
    echo "Deleting old container $line..."
    docker rm -f "$line"
  fi
done

exit 0

起動スクリプトの設定

上記のスクリプトがサーバの起動時にも実行されるようにしておきます。 以下の例は、UbuntuのUpstartを使った例です。 /etc/initの直下に適当な名前(拡張子が.confである必要はあります)で以下のようなファイルを配置します。 なお、respawnが設定されていると、何度でも実行してしまうので、respawnは指定しないでください。

description "SlideHub container"
author "Ryuzee"
start on filesystem and started docker
stop on runlevel [!2345]
script
  bash /usr/local/bin/run_app.sh
end script

Capistranoからデプロイの実行

ここまでやれば、Capistrano3からのデプロイが簡単になります。タスクは以下のようになるでしょう。

set :image_version, ENV['VERSION'] || "latest"
# (中略)
desc 'deploy'
task :deploy do
  on roles(:container) do
    execute "sudo bash /usr/local/bin/run_app.sh #{fetch(:image_version)}"
  end
end

バージョン(というかタグ)を指定してデプロイしたければ、以下のような形で指定すればOKです。

VERSION=1.0.0 bundle exec cap production container:deploy

なお、より詳しいソースは、こちらのレポジトリを見ていただくと良いと思います。

それでは。

アジャイルコーチングやトレーニングを提供しています

株式会社アトラクタでは、アジャイル開発に取り組むチーム向けのコーチングや、認定スクラムマスター研修などの各種トレーニングを提供しています。ぜひお気軽にご相談ください。

詳細はこちら
  • スクラム実践者が知るべき97のこと
  • 著者/訳者:Gunther Verheyen / 吉羽龍太郎 原田騎郎 永瀬美穂
  • 出版社:オライリージャパン(2021-03-23)
  • 定価:¥ 2,640
  • スクラムはアジャイル開発のフレームワークですが、その実装は組織やチームのレベルに応じてさまざまです。本書はスクラムの実践において、さまざまな課題に対処してきた実践者が自らの経験や考え方を語るエッセイ集です。日本語書き下ろしコラムを追加で10本収録
  • プロダクトマネジメント ―ビルドトラップを避け顧客に価値を届ける
  • 著者/訳者:Melissa Perri / 吉羽龍太郎
  • 出版社:オライリージャパン(2020-10-26)
  • 定価:¥ 2,640
  • プロダクト開発を作った機能の数やベロシティなどのアウトプットで計測すると、ビルドトラップと呼ばれる失敗に繋がります。本書ではいかにしてビルドトラップを避けて顧客に価値を届けるかを解説しています。
  • SCRUM BOOT CAMP THE BOOK 【増補改訂版】
  • 著者/訳者:西村直人 永瀬美穂 吉羽龍太郎
  • 出版社:翔泳社(2020-05-20)
  • 定価:¥ 2,640
  • スクラム初心者に向けて基本的な考え方の解説から始まり、プロジェクトでの実際の進め方やよく起こる問題への対応法まで幅広く解説。マンガと文章のセットでスクラムを短期間で理解できます。スクラムの概要を正しく理解したい人、もう一度おさらいしたい人にオススメ。
  • みんなでアジャイル ―変化に対応できる顧客中心組織のつくりかた
  • 著者/訳者:Matt LeMay / 吉羽龍太郎、永瀬美穂、原田騎郎、有野雅士
  • 出版社:オライリージャパン(2020-3-19)
  • 定価:¥ 2,640
  • アジャイルで本当の意味での成果を出すには、開発チームだけでアジャイルに取り組むのではなく、組織全体がアジャイルになる必要があります。本書にはどうやってそれを実現するかのヒントが満載です
  • レガシーコードからの脱却 ―ソフトウェアの寿命を延ばし価値を高める9つのプラクティス
  • 著者/訳者:David Scott Bernstein / 吉羽龍太郎、永瀬美穂、原田騎郎、有野雅士
  • 出版社:オライリージャパン( 2019-9-18 )
  • 定価:¥ 3,132
  • レガシーコードになってから慌てるのではなく、日々レガシーコードを作らないようにするにはどうするか。その観点で、主にエクストリームプログラミングに由来する9つのプラクティスとその背後にある原則をわかりやすく説明しています。
  • Effective DevOps ―4本柱による持続可能な組織文化の育て方
  • 著者/訳者:Jennifer Davis、Ryn Daniels / 吉羽 龍太郎、長尾高弘
  • 出版社:オライリージャパン( 2018-3-24 )
  • 定価:¥ 3,888
  • 主にDevOpsの文化的な事柄に着目し、異なるゴールを持つチームが親和性を高め、矛盾する目標のバランスを取りながら最大限の力を発揮する方法を解説します
  • ジョイ・インク 役職も部署もない全員主役のマネジメント
  • 著者/訳者:リチャード・シェリダン / 原田騎郎, 安井力, 吉羽龍太郎, 永瀬美穂, 川口恭伸
  • 出版社:翔泳社( 2016-12-20 )
  • 定価:¥ 1,944
  • 米国で何度も働きやすい職場として表彰を受けているメンローの創業者かつCEOであるリチャード・シェリダン氏が、職場に喜びをもたらす知恵や経営手法、より良い製品の作り方などを惜しみなく紹介しています
  • アジャイルコーチの道具箱 – 見える化の実例集
  • 著者/訳者:Jimmy Janlén / 原田騎郎, 吉羽龍太郎, 川口恭伸, 高江洲睦, 佐藤竜也
  • 出版社:Leanpub( 2016-04-12 )
  • 定価:$14.99
  • この本は、チームの協調とコミュニケーションを改善したり、行動を変えるための見える化の実例を集めたものです。96個(+2)の見える化の方法をそれぞれ1ページでイラストとともに解説しています。アジャイル開発かどうかに関係なくすぐに使えるカタログ集です
  • カンバン仕事術 ―チームではじめる見える化と改善
  • 著者/訳者:原田騎郎 安井力 吉羽龍太郎 角征典 高木正弘
  • 出版社:オライリージャパン( 2016-03-25 )
  • 定価:¥ 2,138
  • チームの仕事や課題を見える化する手法「カンバン」について、その導入から実践までを図とともにわかりやすく解説した書籍。カンバンの原則などの入門的な事柄から、サービスクラス、プロセスの改善など、一歩進んだ応用的な話題までを網羅的に解説します。
  • Software in 30 Days スクラムによるアジャイルな組織変革“成功"ガイド
  • 著者/訳者:Ken Schwaber、Jeff Sutherland著、角征典、吉羽龍太郎、原田騎郎、川口恭伸訳
  • 出版社:アスキー・メディアワークス( 2013-03-08 )
  • 定価:¥ 1,680
  • スクラムの父であるジェフ・サザーランドとケン・シュエイバーによる著者の日本語版。ビジネス層、マネジメント層向けにソフトウェア開発プロセス変革の必要性やアジャイル型開発プロセスの優位性について説明
  • How to Change the World 〜チェンジ・マネジメント3.0〜
  • 著者/訳者:Jurgen Appelo, 前川哲次(翻訳), 川口恭伸(翻訳), 吉羽龍太郎(翻訳)
  • 出版社:達人出版会
  • 定価:500円
  • どうすれば自分たちの組織を変えられるだろう?それには、組織に変革を起こすチェンジ・マネジメントを学習することだ。アジャイルな組織でのマネージャーの役割を説いた『Management 3.0』の著者がコンパクトにまとめた変化のためのガイドブック