DockerコンテナをCapistranoでデプロイ このエントリをはてなブックマークに登録

2014年11月13日

irohirokiirohiroki

開発環境と運用環境の差異

Railsアプリの開発をMacでしている人は多いと思います。しかし本番では大抵Linuxマシンで運用するため、実行環境の違いから問題が発生することがあります。特にサードパーティ製ライブラリやツールを使う場合、MacとLinuxで同じ動作をする保証はどこにもありません。また、開発にLinuxマシンを使っていたとしても、本番と全く同じ構成で開発するのは難しいでしょう。

Dockerを使うと開発から運用まで同じ環境を使え、しかもハードウェア仮想化よりも遥かに軽量です。そこで、私が今KRAYで担当しているプロジェクトでは開発から本番まで全ての環境でDockerを使えるようにしました。

それぞれの環境で解決すべき課題がありましたが、今日は本番環境にデプロイする仕組みを紹介します(KRAYでインテグレーション環境と呼ばれる環境についてはDockerホストをプロジェクトや権限で分けるをご覧ください)。

cap-deploy-docker

Dockerイメージ作成まで

今回対象にしたシステムは以下のような性質のものでした。

  • 単純なRailsアプリケーション
  • アプリケーションサーバは1〜2インスタンス
  • データベースは別のホストにある

デプロイに必要な処理は以下の通りです。

  • Dockerイメージの作成
  • DBマイグレーション
  • Assets precompilation
  • Dockerコンテナの起動
  • Webフロントエンドとの接続
  • 古いDockerコンテナの停止

Dockerイメージの作成

Dockerイメージを作成する場所によって、必要な処理やデプロイにかかる時間は大きく違います。選択肢としては次のようなものが考えられるでしょう。

  • Docker Hub
  • Quay.ioなどのDockerリポジトリ
  • 開発用マシン
  • デプロイ先のサーバ
  • それ以外のどこか

今回のシステムはある種のXML配信システムで、以下のような条件がありました。

  • プライベートリポジトリの予算はない
  • 余分なサーバの予算もない
  • 本番サーバの負荷は軽い

よって今回はデプロイ先のサーバ上で作成することにしました。

ソースコードの取得

デプロイ用のイメージを作るにはソースコードが必要です。ソースコードはGitHubのプライベートリポジトリにあるので、何らかの方法でサーバからプライベートリポジトリにアクセスしなければなりません。

デプロイ用の鍵を用意するか、SSH agent forwardingを使うか……と、考え始めたところで気づきました。そういう仕事をしてくれる既存のツールに。もちろんCapistranoです。

Capistranoはリポジトリとサーバだけ指定すればソースコードを取り寄せてくれます。それが素のCapistranoの動作で、フレームワークやアプリケーションに特有の処理はプラグインや独自のスクリプトで行うようになっています。

以上のような経緯で、今回のテーマの一つであるCapistranoが登場することになりました。

docker build

イメージ作成のcap taskは以下のようになりました。ソースコードのルートにDockerfileがある前提です。

set :pty, true

namespace :deploy do
  namespace :docker do
    desc ‘Build image’
    task :build do
      on roles(:app) do
        within release_path do
          sudo :docker, "build", "-t", fetch(:application), "."
        end
      end
    end

    after ‘deploy:updating’, ‘deploy:docker:build’
  end
end

内容は、アプリケーション名をタグにしてdocker buildするだけです。sudoを実行するにはttyが必要なので、set :pty, trueしています。

全てのデプロイスクリプトを含んだサンプルアプリケーションはgithub.com/irohiroki/docker_capistrano_rails_sampleにあります。

残りのデプロイプロセス

DBマイグレーションとAssets Precompilation

DBマイグレーションもAssets precompilationもrakeタスクを実行するだけですが、Capistranoを使うならcapistrano-railsを利用できないか調べたところ、下のようにrakeコマンドをDockerコンテナの中で実行するように置き換えることで活用できることがわかりました。

namespace :deploy do
  namespace :docker do
    desc ‘Make rake run in container’
    task :map_rake do
      SSHKit.config.command_map[:rake] = "sudo docker run –rm #{docker_run_opts.join(‘ ‘)} rake"
    end
    before ‘deploy:updated’, ‘deploy:docker:map_rake’
  end
end

上のように、rakeコマンドとして使いたいコマンドラインをSSHKit.config.command_map[:rake]にセットすることで置き換えられます(docker_run_optsは必要なオプションを返すヘルパーメソッドです。詳細はソースコードを見てください)。あとはcapistrano-railsが適切なタイミングでDBマイグレーションとAssets precompilationを実行してくれるというわけです。

なお、SSHKit.config.command_map[:rake]=taskブロックの外に書くと、デプロイスクリプトの書き方によっては上手くいきません。なぜなら、taskブロックの外はスクリプトのロード時に評価されるため、docker_run_optsが必要とする値が存在しない場合があるからです。

Dockerコンテナの起動

Dockerコンテナの起動も、taskだけ見ると特に難しいことはしていません。ただ、後でコンテナを区別する必要があるので、コンテナIDをcidという識別子で記憶しています。同様に、アプリケーションサーバにマップされたポート番号をbindされたIPアドレスと一緒にhost_portという名前で保存しています。こちらはWebフロントエンドと接続するときに使います。

namespace :deploy do
  namespace :docker do
    desc ‘Run application container’
    task :run do
      on roles(:app) do
        set :cid, capture(:sudo, "docker", "run", "-d", "-P", *docker_run_opts)
        set :host_port, capture(:sudo, "docker", "port", fetch(:cid), 3000)
      end
    end
  end
  desc ‘Restart application’
  task :restart do
    invoke ‘deploy:docker:run’
  end
end

deploy:restartはCapistranoに用意されているタスクで、ソースコードの更新やシンボリックリンクの更新などが終わった後に呼ばれることになっています。それを利用してdocker runを実行しています。

Webフロントエンドとの接続

今回のシステムのフロントエンドはNginxを使ったリバースプロキシだったので、Nginxの設定ファイルを上で覚えておいたhost_portを使って書き換え、リロードするタスクを書きました。ここはシステムに大きく依存するので詳細は省きます。

古いDockerコンテナの停止

古いコンテナを止めるには、コンテナIDが必要です。そこで、デプロイ毎にコンテナIDをログに記録することにしました。コンテナ停止とログのタスクは次の通りです。

namespace :deploy do
  namespace :docker do
    desc ‘Stop old container’
    task :stop do
      on roles(:app) do
        if test "[ -e #{fetch(:containers_log)} ]"
          old_cid = capture(:tail, "-1", fetch(:containers_log)).split(‘ ‘).last
          test :sudo, "docker", "stop", old_cid  # ignore failure
        end
      end
    end
    desc ‘Log container id’
    task :log do
      on roles(:app) do
        execute %|echo "Tree #{fetch(:current_revision)} run in #{fetch(:cid)}" >> #{fetch(:containers_log)}|
      end
    end
    after ‘deploy:docker:run’, ‘deploy:docker:stop’
    after ‘deploy:docker:run’, ‘deploy:docker:log’
  end
end

deploy:docker:stopではログの最後の行からコンテナIDを取得し、docker stopを実行しています。このとき、stopが成功しても失敗しても良いようにtestディレクティブを使っています。何らかの原因でコンテナが停止してしまうことはありますが、その場合にstopの失敗を許容しないとデプロイスクリプトが停止してしまうためです。

deploy:docker:logではログメッセージを生成して追記しています。ログにはデプロイしたバージョンのコミットID(SHA-1)も含めることで追跡性を高めています。

以上がデプロイに最低限必要な処理になります。

まとめ

今後の展開

コンテナとイメージの掃除

古いコンテナは止めてるだけなので、停止した状態で残っています。直近のものは調査のために多少残しておく必要があるかもしれませんが、さらに古いものは消さなければなりません。

ロールバック

今回のバージョンではロールバックは考慮していません。プロジェクトの方針としては、前のバージョンをデプロイしたら(動作は)元に戻るように作っています。

他の場所でdocker build

当然ながら本番サーバ上でdocker buildできるプロジェクトばかりではないので、イメージを作成する場所を選択できるようにしたいと考えています。

最後に

ここまで読んでくださりありがとうございます。KRAYのFacebookページで「いいね!」していただけると、今後の活動をタイムラインでお知らせできます。よろしくお願いします!

  1. メモからはじめる情報共有 DocBase 無料トライアルを開始
  2. DocBase 資料をダウンロード

「いいね!」で応援よろしくお願いします!

このエントリーに対するコメント

コメントはまだありません。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)


トラックバック

we use!!Ruby on RailsAmazon Web Services

このページの先頭へ