こんにちは。Tokyo Otaku Mode(以下 TOM)ソフトウェアエンジニアの稲田です。
TOM が運営している otakumode.com は現在 1 日約 3、4 回ほどの頻度で更新されています。
これを多いと見るか少ないと見るかは人によって違うと思いますが、デプロイのたびにサイトにアクセスできない状態になっていては、まともなサイト運営とは言えないでしょう。
そこで、サイトへのアクセスを一瞬も止めることなくデプロイする、いわゆるホットデプロイと呼ばれるものが必要になります。
TOM ではアプリケーションサーバーに Node.js を使っており、既に Cluster モジュールを使った独自のホットデプロイの仕組みが実装されています。
今回はこのホットデプロイを Docker と Node.js でもできるようにしていきます。
Docker
Docker とは Docker, Inc.(旧 dotCloud, Inc.)が開発しているオープンソースの仮想化のためのソフトウェアです。
従来の仮想化と違うところはコンテナと呼ばれる単位で管理され、仮想マシンが必要ないのでオーバーヘッドがほとんどなくパフォーマンスに優れています。
また、ビルドした Docker コンテナを別の Docker が動くマシンに持っていっても動作させることができるため、1 度コンテナをビルドすれば複数台のサーバーで同じ Docker コンテナが動かせます。これはアプリケーションサーバーのデプロイに向いていると思いませんか?
これ以上の Docker についての説明はここでは割愛しますが、もっと詳しく知りたい場合は 公式ドキュメント を見てください。
必要なもの
- Docker
- Linux カーネル 3.9 以降
- Nginx 1.9.1 以降
- Node.js
今回必要なものとして Node.js を上げましたが、他のアプリケーションサーバーにも応用できます。
ホットデプロイの概要
Docker コンテナを事前にビルドしてサーバーにデプロイするという方式を取る際にも必要になるのが冒頭でも書いた、無停止でのデプロイ、ホットデプロイです。
しかし Docker コンテナは単なる環境なので、中で実行するプログラムが必要です。
TOM では主に Node.js を使用しているので、以降は Node.js で作成したアプリケーションサーバーが Docker コンテナで実行されると仮定して進めます。
単純に Docker コンテナを停止/起動させた場合、アプリケーションサーバーも一緒に停止するため停止してから起動するまでの間に接続できない時間が発生してしまいます。
そこで、新しい Docker コンテナを起動してから古い Docker コンテナを停止させることができれば、接続できない時間を発生させることなくデプロイできます。
例えば現在動いている Docker コンテナを A
、新しく起動する Docker コンテナを B
とすると
A 停止 → B 起動
ではなく
B 起動 → A 停止
とできれば良いということです。
通常のホットデプロイは、あるポートを bind しているソケットを複数のアプリケーションサーバーで共有することによって上記を実現しますが、今回は SO_REUSEPORT を使います。
SO_REUSEPORT
SO_REUSEPORT は Linux カーネル 3.9 から入った機能で、これを使うことによって同じポートで待ち受けるアプリケーションを複数立ち上げられるようになります。
しかしながら 2015/12/17 現在の Node.js v5.2.0 は Linux の SO_REUSEPORT に対応していません。
そこで、SO_REUSEPORT に対応している Nginx(バージョン 1.9.1 以降)をリバースプロキシとして配置して、Node.js アプリケーションサーバーを upstream にすることで解決します。
Examples
実際にホットデプロイを行うために用意したファイルと設定について解説していきます。
ファイル構成は以下の通りです。
Dockerfile
app.js
今回は簡単に特定の文字列を返すだけのアプリケーションです。
前述の通り Node.js は SO_REUSEPORT に対応していないので、ここで
などとすると、新しい Docker コンテナを起動したときに同じ 4000 番ポートを bind しようとしてエラーになり起動しません。
それを回避するために Unix Domain Socket を使い、後述の nginx.conf でこの Unix Domain Socket を指定します。
また、完全に起動した後に app.pid
を書き出すようにして、アプリケーションサーバーが起動したかどうかを run.sh から確認できるようにしておきます。
nginx.repo
Nginx 1.9 以降が必要なので mainline を使います。
nginx.conf
ポイントは
listen 3000 reuseport;
として SO_REUSEPORT を使うようにする
- バックエンドのアプリケーションサーバーとのやり取りは Unix Domain Socket を使う
daemon off;
としてフォアグラウンドで動作させるようにする
です。
1. listen 3000 reuseport;
として SO_REUSEPORT を使うようにする
説明するまでもないですね :)
2. バックエンドのアプリケーションサーバーとのやり取りは Unix Domain Socket を使う
app.js は Unix Domain Socket で待ち受けているので、Nginx 側もそれに合わせます。
3. daemon off;
としてフォアグラウンドで動作させるようにする
Nginx を Docker のメインプロセスとして動作させるため、daemon off;
とします。
run.sh
app.js が起動した後に書き出す app.pid
ができるのを待ってから Nginx を起動するようにします。
そうすることによって Nginx が先に起動して Bad Gateway になってしまう問題を防ぎます。
app.js を起動する部分は forever などを使ってもいいでしょう。
また、これは Docker コンテナのメインプロセスとなるスクリプトなので実行権限を付けておきます。
ホットデプロイの実践
Docker コンテナのビルド
ここではわかりやすいように hotdeploy_app
というタグを付けています。
Docker コンテナの起動
先ほどビルドした Docker コンテナを起動します。
--net=host
オプションを付けることでホスト側のネットワークスタックをそのまま使うようしています。
これでホスト側が Linux カーネル 3.9 以上であればコンテナ側でも SO_REUSEPORT が使えるようになります。
--net=host
オプションを付けて起動しているので localhost に対して接続テストをします。
Docker コンテナのホットデプロイ
それではいよいよ Docker コンテナ自体をホットデプロイしてみます。
restart.sh
という名前で下記の内容のシェルスクリプトを用意します。
restart.sh
を実行する前に、現在起動している Docker コンテナの ID を確認しておきます。
789b134a613d
が現在起動している Docker コンテナの ID です。
次に、ホットデプロイができているかを検証するために ab
コマンドでリクエストを送りつつ別のターミナルで restart.sh
を実行します。
Failed requests
が 0 になっているので、失敗したリクエストがないことがわかります。
現在起動している Docker コンテナも確認してみましょう。
789b134a613d
から a3b1d183c018
にコンテナ ID が変わっており、ホットデプロイができていることがわかります。
まとめ
Docker + Node.js でホットデプロイが実現できることがわかりました。
TOM ではまだ検証段階ですが、今後の検証結果次第では十分実用できるのではないかと思います。
それでは、良い Docker ライフをお送りください。
参考