Docker を利用した Web アプリケーションのデプロイ

技術部の鈴木 (id:eagletmt) です。

クックパッドでは一部の Web アプリケーションサーバで Docker が使われており、今回はそのデプロイ方法について紹介します。

Docker で Web アプリケーションをデプロイするときには、まだまだベストプラクティスがある状況ではありません。 たとえば、どのように無停止でデプロイするか、どのようにコンテナと通信するかといった問題があります。 最初に Apache Mesos と Marathon などのツールを検証しましたが、クックパッドの環境において使いやすそうなものはなく、最終的に自前でデプロイのしくみを作ることにしました。 しかし Docker 周辺のツールは様々な新しいものが出てきている最中です。 今はまだベストなものが無いけれども、近いうちによりよいものが出てくるかもしれません。 そのため、できるだけ単純なしくみにしておくことで、後から別のしくみに移行しやすいようなものにしようと意識しました。

デプロイの流れ

Docker 単体では無停止でのデプロイを実現するのは難しいため、各ホストに nginx を立てて、デプロイ時に nginx の設定を更新する形にしています。 全体の構成は下図のようになっています。

デプロイは以下の流れで進みます。 既に v1 が稼動しており、新しく v2 をデプロイするとします。

  1. docker pull v2-image
  2. docker run --detach v2-image
  3. v2-container のヘルスチェックが通るまで待つ
  4. nginx の設定を v1-container から v2-container へ書き換えて nginx を reload
  5. コネクションのタイムアウト分だけ待つ
  6. docker stop v1-container

Docker を利用した Web アプリケーションのデプロイ方法としてはわりと一般的な方法かと思います。 とはいえ、この中でいくつか考えなければならない問題があります。具体的には、通信方法、設定値、権限の問題です。

通信方法

一つのホストに複数のコンテナを立てられるようになっており、リクエストをどのコンテナにプロキシするかはバーチャルホストで行っています。 nginx とコンテナ間の通信方法として、2つの方法を用意しています。

UNIX ドメインソケットを利用する

docker run のときに適当なディレクトリを --volume でマウントし、コンテナ内の Web アプリケーションはそのディレクトリに UNIX ドメインソケットを作成して listen します。 するとホスト側にもその UNIX ドメインソケットが見えるため、そこへプロキシする方法です。 コンテナのネットワークを分離でき、オーバーヘッドが小さく、ポートが被る問題に悩まなくてよいため、基本的にはこの方法を使うようにしてもらっています。

Docker の bridge networking を利用する

docker run--publish オプションを渡し、そこで指定したポートへとプロキシする方法です。 この方法は Docker イメージが公開されているソフトウェアをそのま利用するとき等に使います。 ホスト側のポート番号は、現在のホスト内で動いている Docker コンテナが使っているポート番号のうち最も大きなものに 1 を足したものを使うようにしています。 雑な割り当て方法ですが、これでもほとんどの場合でうまく動きます。

設定値

Docker コンテナに設定値を外から与えたい場合、環境変数が最も楽な方法です。 問題はその環境変数をどのように管理し、どのように与えるかです。

環境変数の管理には etcd を利用しています。 etcd にアプリケーション毎に環境変数の値を保存し、etcenv を使って環境変数を一時ファイルへ出力し、それを docker run --env-file で読み込ませる、というようにしています。

権限

Docker は root ユーザで動いており、Docker コンテナを操作するためには root 権限が必要です。 Docker デーモンの UNIX ドメインソケットを root 以外も書き込めるようにしたり、あるいは tcp で bind すれば root 権限がなくてもコンテナを操作できますが、 docker run --volume オプションを自由に使われてしまうとホスト側で root しか読み書きできないファイルもコンテナ内から閲覧できてしまう等の問題があります。

権限の問題を解決するため、デプロイに必要な操作をすべて行うデプロイスクリプトをホスト側に用意し、そのデプロイスクリプトに対してのみ全ての開発者に sudo を許可する、という形にしています。 またデプロイがホスト側で完結するので、何かのオペレーションでデプロイが必要になったときに、コマンド一つで再デプロイできるのも利点です。 開発者がデプロイするときは、Capistrano 経由でこのデプロイスクリプトを実行しています。

今後

このしくみでデプロイ自体はうまくできているものの、全体で見ると完全には自動化できていない部分があります。 たとえば現状では、デプロイ先のホストを決定するために EC2 のタグを使っており、このタグは運用者が設定しています。 つまり、どのアプリケーションをどのホストにデプロイするかを運用者が指定している状況です。 Docker によって得られたポータビリティを活かして、必要なメモリ量やコンテナ数などを設定すれば適当なホストにデプロイされるようになるとよさそうです。 さらに動的にコンテナ数を変えると自動的にスケールイン・スケールアウトしてほしいです。 その実現のために、Amazon EC2 Container Service がいいのか、swarm がいいのか、あるいは自前で何か作ったほうがいいのか、色々と検討中です。

まとめ

できるだけ単純なしくみにして後からもっとよいツールが出てきたときに対応しやすいようにしつつ、現状ではどのようにして Docker を利用した Web アプリケーションをデプロイしているか紹介しました。 Docker を利用した大まかなインフラ構成や利用しているミドルウェアの話はよく見ますが、Web アプリケーションのデプロイ方法についての具体的な話はあまり見ないので、一つの参考になればと思います。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/