開発環境と運用環境の差異
Railsアプリの開発をMacでしている人は多いと思います。しかし本番では大抵Linuxマシンで運用するため、実行環境の違いから問題が発生することがあります。特にサードパーティ製ライブラリやツールを使う場合、MacとLinuxで同じ動作をする保証はどこにもありません。また、開発にLinuxマシンを使っていたとしても、本番と全く同じ構成で開発するのは難しいでしょう。
Dockerを使うと開発から運用まで同じ環境を使え、しかもハードウェア仮想化よりも遥かに軽量です。そこで、私が今KRAYで担当しているプロジェクトでは開発から本番まで全ての環境でDockerを使えるようにしました。
それぞれの環境で解決すべき課題がありましたが、今日は本番環境にデプロイする仕組みを紹介します(KRAYでインテグレーション環境と呼ばれる環境については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を使うことで、軽量でポータブルなアプリケーション実行環境を構築できる
- Capistranoとcapistrano-railsの機能を活用することでデプロイスクリプトも簡潔に書けた
今後の展開
コンテナとイメージの掃除
古いコンテナは止めてるだけなので、停止した状態で残っています。直近のものは調査のために多少残しておく必要があるかもしれませんが、さらに古いものは消さなければなりません。
ロールバック
今回のバージョンではロールバックは考慮していません。プロジェクトの方針としては、前のバージョンをデプロイしたら(動作は)元に戻るように作っています。
他の場所でdocker build
当然ながら本番サーバ上でdocker buildできるプロジェクトばかりではないので、イメージを作成する場所を選択できるようにしたいと考えています。
最後に
ここまで読んでくださりありがとうございます。KRAYのFacebookページで「いいね!」していただけると、今後の活動をタイムラインでお知らせできます。よろしくお願いします!
このエントリーに対するコメント
日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)
- トラックバック
「いいね!」で応援よろしくお願いします!