Docker で Web アプリを運用してみた
Docker してますか!
実は実験的に Docker で Web アプリを数ヶ月運用しており、色々と試行錯誤してきたので、少しずつアウトプットしていきます。
ちなみに Ruby 製のアプリで、AWS の EC2 上で運用している、小〜中規模ぐらいのものです。
2014-06-16 16:00: 追記あり
Docker イメージのビルドについて
Dockerfile を普通に書いてます。
今のところ、2層構造にしていて、
- ベースとなるイメージ
- Ruby
- アプリケーションサーバー (Puma)
- アプリケーションのソース (git clone)
- bundle install
- デプロイされるイメージ (ベースイメージを元に作る)
- git pull してソース更新
- bundle install し直してベースにない gem を入れる
- asset の precompile
という感じでやってます。
ベースイメージは1日1回、デプロイイメージは master への push を trigger にビルドしていて、ビルドは Jenkins でシェルスクリプトでゴリゴリやってます。
Jenkins のビルドID (日時) をタグとしてレジストリに push するようにしています。
レジストリについて
プライベートレジストリを自前でホストしてもいいんですが、運用面倒なので quay.io を使ってました。
quay.io は、UI が便利だったり、ボット用のトークンが簡単に払い出せたりできて便利です。
しかし、Docker Hub も発表されましたし、料金も安いので公式リポジトリ (index.docker.io) に移行中です。
(機能的には、push して pull するだけなら、どちらも大差ないので…)
イメージをサービスごとに細かく分割していくと、あっという間にレジストリ数の上限に引っかかるのが悩みどころ。妥協して supervisord などで1つにまとめていくのもアリだと思います。
ログ収集について
コンテナは使い捨てが基本なので、ログはどこかに退避させる必要があります。
docker run
のオプションで -v
や --volumes-from
を使うとホスト側ディレクトリをマウントできるので、そちら経由でホスト側に退避させる事もできるのですが、ホスト側も使い捨てたいので、fluentd を使っています。
構成はベーシックな fluentd + ElasticSearch + kibana で、それぞれコンテナ立てて動かしています。(kibana は nginx をサーバーに)
ホットデプロイについて
一番悩んだのがアプリのデプロイ。
実行については docker pull して docker run でいいとして、ダウンタイム無しで切り替えるのはどうしようか、という感じです。
どうせなら Blue-Green Deployment もしたいので、いくつか方法を試してみました。
- Elastic Load Balancer で EC2 インスタンス抜き差し
- Elastic Beanstalk の Docker デプロイを利用
- 最近 Beanstalk で Docker デプロイが出来るようになったので、試してみた
- Beanstalk では DNS の CNAME レコード書き換えで Blue-Green ぽい事ができる
- 利点
- デプロイが簡単 (レジストリに push 済みなら JSON を送るだけでデプロイできる)
- ツールがある: eb_deployer
- 欠点
- DNS 書き換えでの切り替えなのでクライアントのキャッシュ依存
- http://example.com/ のようなサブドメインの無い URL で運用できない(Route53 の Alias が Beanstalk の CNAME レコードをサポートしていないため ELB が使えない)
- デプロイに時間が掛かる:Beanstalk のデプロイが EC2 インスタンスを1から立ち上げるため
で、両方とも結局 EC2 インスタンスの立ち上げが遅いという結論にいたり、どうせ docker 使っていてコンテナの immutability は確保できるので、ホストは使い回す事にしました。
nginx + mruby で backend 書き換え
良い方法はないかなーとググっていたところ、Docker, Mesos, Sensu等を利用したBlue-Green Deploymentの仕組み を見つけて パク 参考にさせて頂く事にしました。
結局のところ、リバースプロキシである nginx の後ろのアプリだけ切り替えられればいいので、nginx の設定を動的に書き換える事にしました。(immutable とは一体…うごごご)
せっかくなので、面白そうだった ngx_mruby (組み込み用 Ruby である mruby を nginx で使えるようにする拡張) を使って、アプリと別ポートで設定書き換える API を作り、デプロイ時に叩くようにしました。
server { listen 8888; server_name ctrl; location = /backends { mruby_content_handler /etc/nginx/mruby/backends.rb; } }
BACKEND_CONF = '/etc/nginx/backend.conf' r = Nginx::Request.new if r.method == 'POST' json = r.args.gsub(/%[0-9a-fA-F]{2}/) { |s| s.slice(1..-1).to_i(16).chr } begin backends = JSON.parse(json) rescue ArgumentError => e Nginx.errlogger Nginx::LOG_ERR, "Invalid json received: #{e.message}" Nginx.return Nginx::HTTP_BAD_REQUEST return end conf = "# Generated at #{Time.now}\n" conf << "upstream backend {\n" backends.each { |backend| conf << " server #{backend} fail_timeout=0;\n" } conf << "}\n" File.open(BACKEND_CONF, 'w') { |f| f.write(conf) } Nginx.errlogger Nginx::LOG_INFO, "Updated backend conf to: #{json}" else if File.exist?(BACKEND_CONF) conf = IO.read(BACKEND_CONF) else conf = '' end end r.content_type = 'text/plain' Nginx.echo conf
そして、この API をデプロイ時に叩きます。
# アプリコンテナのプライベート IP 取得 CONTAINER_IP=$(docker inspect -f '{{ .NetworkSettings.IPAddress }}' $APP_CONTAINER_NAME) # API 叩いて backend.conf を書き換え BACKENDS=$(ruby -rjson -ruri -e 'puts URI.encode_www_form_component(ARGV.to_json)' "$CONTAINER_IP:$APP_PORT") curl -X POST 'http://localhost:8888/backends?'$BACKENDS # HUP 送って設定反映 docker kill -s HUP app-nginx
しかし動くんだよなぁ、これが。しかも切り替えは一瞬で済むという…。
これにより、デプロイに掛かる時間はほぼ docker pull 分だけで済むようになりました。
欠点としては、同じ EC2 インスタンスを使い回すのでメンテナンスが必要になる事ですね…
Docker の安定性について
Docker 自体は今のところ安定して稼働できています。
Docker 1.0 もリリースされましたし、まだ気になるところもあるのですが(docker pull 中に Ctrl-C すると docker pull できなくなる事があるとか)、そろそろ実用いけるんじゃないでしょうか。
もうちょっと運用してみたいと思います。
追記: 2014-06-16 16:00
ngx_mruby の開発者である id:matsumoto_r さんに反応していただき、違う構成のエントリも書かれていたのでご紹介がてら、twitter 上でのやりとりを引用します。
Dockerとmrubyで迅速かつ容易にnginxとapacheの柔軟なリバースプロキシ構成を構築する
Docker の --link
を使って、コンテナ間接続をして nginx → アプリ を構成する例をご紹介されています。
私も、この構成は試していたのですが、Docker の --link
が仕様上、起動時に静的にコンテナ間の接続を決定してしまうため、デプロイ毎に nginx コンテナを再起動する必要があり、ダウンタイムが発生するため、見送った経緯がありました。上記の ELB のインスタンス抜き差しなどインスタンス単位でのダウンタイムを別レイヤで吸収できるのであれば、Docker の本流というべき綺麗な構成にできると思います!
こちらについて、コメントしたところ、twitter 上で反応いただけました。
この構成、デプロイの度に nginx コンテナを再起動する必要があり、ダウンタイムが発生するのが難点であきらめ、設定ファイルの動的書き換えに逃げました… http://t.co/AVyrBwf1vx / “人間とウェブの未来 - …” http://t.co/UrFBzcRuHb
— Kota Saito (@ksaito) 2014, 6月 16
@ksaito この構成だと複雑な事をせずに構築できるという利点はありますが、御指摘の問題はありますね。動的書き換えの記事拝見しましたが非常に面白かったです。書き換え対象を例えばRubyスクリプトにしてしまえば、nginxにHUPすることなく振り分ける事も可能です。
— MATSUMOTO, Ryosuke (@matsumotory) 2014, 6月 16
@matsumotory ありがとうございます>< できれば設定ファイル書き換え無しで済ませたくて、最初は Userdata を使ってコンテナIPを記録して、動的に mruby で backend を選ぶ、というのを書いたんですが、Userdata が定期的に飛んでしまうっぽく…
— Kota Saito (@ksaito) 2014, 6月 16
@ksaito おお、そんな事が…すみません。GCで回収されてしまっているのかもしれませんね。よろしければissueに上げていただければ対応したいと思います。一方で例えば、RedisやVedisからbackendsの情報を引き出すという方法も良いかもしれません。
— MATSUMOTO, Ryosuke (@matsumotory) 2014, 6月 16
@matsumotory あまり詳しく検証してなくて申し訳ないのですが nginx のプロセスが定期的に入れ替わってるのかなと、仕様の範囲かと思っていました orz Vedis / Redis も検討したのですが、だったら設定ファイル書き換えいいかな、と落ち着いた感じですw
— Kota Saito (@ksaito) 2014, 6月 16
ただ、設定ファイル書き換えにも難点があって、Docker コンテナを再起動すると巻き戻るので、やはり Redis 等の外部ストレージを使うか、Docker の --volume-from や -v で設定ファイルを外部に保持する必要がありそうです。
— Kota Saito (@ksaito) 2014, 6月 16
@ksaito そうですね。ブログの構成の利点は、リクエストの度にbackend選択のスクリプトを実行しなくてよくてサーバプロセスとmrubyインタプリタの性能やメモリの問題を意識しなくて良いので、HUPが許されていれば良い選択だと感じました。
— MATSUMOTO, Ryosuke (@matsumotory) 2014, 6月 16
@ksaito Userdataのバグ?は今ざっとコード見たところ、最初にuserdataのhashオブジェクトを作る時にオブジェクトをgc_protect()してやれば良いのかな?とも思いましたが、nginxの動作も関係しているのかもしれないので難しいですね…
— MATSUMOTO, Ryosuke (@matsumotory) 2014, 6月 16
@matsumotory ありがとうございます。条件を再現できたら issue として挙げさせていただきます!
— Kota Saito (@ksaito) 2014, 6月 16
@ksaito あ、誤読かもしれませんが、もしuserdataの使い方がPOSTしてその値をuserdataに入れて...という流れであれば、それだと受けたnginxプロセスのみのuserdataにデータが入るので、他のプロセスには有効ではない、という仕様はあります。
— MATSUMOTO, Ryosuke (@matsumotory) 2014, 6月 16
@matsumotory おっと… まさしくその仕様にハマった気がします…orz そりゃあワーカープロセスごとに独立ですよね orz となると、やはり Userdata はキャッシュ用途にだけ使うのが無難ですね…
— Kota Saito (@ksaito) 2014, 6月 16
@ksaito まだまだmod_mrubyやngx_mrubyもその周辺のgemも使ってみて気付くことが多いので、ぼくも色々試してみたいと思います。色々やれるようになった分、ハマる所も多そうですね。
— MATSUMOTO, Ryosuke (@matsumotory) 2014, 6月 16
相変わらずボケが発動しておりますが orz matsumoto-r さん、ありがとうございました!
ngx_mruby 使うと Ruby であんなことやこんなことができて楽しので、皆使うべきそうすべき!