この記事ははてなエンジニアアドベントカレンダー2015の1日目です。今回は、既存の運用フローに乗せやすいDockerイメージへのchrootによるデプロイの考え方と自作のコンセプトツール droot を紹介します。
背景
Dockerがリリースされてから3年近く経過しました。 Web界隈において、確か初期には、テスト環境をすばやく作れて便利な高速に動くVagrantのようなものという扱いだったと思います。そこから、ローカルやCIで作成したコンテナをイメージ化し、本番環境までもっていけるというポータビリティの高さが注目されました。 とはいえ、実際にDockerコンテナを本番環境、特にアプリケーションサーバとして動作させるためには、いくつもの課題があります。 2年前はまだいつか本番に投入するぞぐらいの気持ちでした。当時のブログエントリの様子です。
- Docker + Mesos + Marathon + Graphite + Fluentd + Sensuを組み合わせたデプロイ管理ツールの話 - ゆううきブログ
- Docker 使ってたらサーバがゴミ捨て場みたいになってた話 #immutableinfra - ゆううきブログ
しかし、Docker自体がリリースを重ねて機能が増えて、動作が安定してきた結果、本番環境での利用が現実的になってきているかもしれません。 実際に、本番環境に導入された事例もいくつか見聞きしています。
- Docker Meetup Tokyo #4 - Docker at Wantedly // Speaker Deck
- 'D'evelopment and 'D'eployment with 'D'ocker at 'D'wango // Speaker Deck
- Docker を利用した Web アプリケーションのデプロイ - クックパッド開発者ブログ
- Flexible Blue Green Deploymentのススメ|サイバーエージェント 公式エンジニアブログ
Docker 本番導入の課題
変更があればコンテナを捨てて新しいコンテナを立ち上げるImmutable Infrastructure要素と、1度作成したホストを使いまわしていく既存の運用フローと、どうすり合わせるかがDocker導入の壁です。 少なくとも僕が課題だと認識しているもしくはしていたのは以下のようなものです。(他にもあった気がしますがとりあえず今思い出せる範囲で)
- ChefやAnsibleとの兼ね合い
- Dockerイメージの管理
- Dockerコンテナの監視
- コンテナのゴミ掃除
- Server::Starter動かないんでは問題
- ネットワークまわりのパフォーマンス劣化(パフォーマンスの観点からみるDockerの仕組みと性能検証 #dockerjp - ゆううきブログ)、docker
- docker コンテナ内の問題調査の手間
- dockerコンテナ内のログの取り扱い
- docker pull が遅い
- storage driverがdevice mapperのとき、
docker build
とdocker run
がたまに失敗する問題 - dockerコンテナ内でたまに名前解決が失敗する
特に、本番に導入するということで、ゼロダウンタイムでのデプロイをどうするかが問題です。 よくみかける基本的なアイデアは、デプロイするたびに、Dockerコンテナを現在稼働中のコンテナとは別にもう1セット構築し、前段のロードバランサでそちらに切り替える(スタティックデプロイ)というものです。いわゆるBlue Green Deployment的な手法ですね。 その他は、より高度なクラスタマネージャやスケジューラを用いた手法があります。Dockerで実現するゼロダウンタイムデプロイ
Docker 導入の目的
1つ1つの課題は手間をかけたりDocker自体が成熟すれば解決するかもしれませんが、これらを同時に相手するのは非常にやっかいだと感じていました。解決したとしても、解決のために作った仕組みの運用コストも増えます。 そもそもDockerを使って何がやりたかったのか、ポータビリティだとかコンテナを毎回捨ててクリーンな状態が保てるとかいろいろなDocker導入のメリットはありますが、なんとなくDockerが謳っているメリットを鵜呑みにして、それに振り回されているのではないか、本当に目の前の環境に導入して価値がでるのか、といったことを考えるようになりました。
そこで、どんなときにDockerが欲しかったかを振り返ると、Linuxディストリのパッケージ依存関係に苦しんでいるときだったり、アプリケーションエンジニアがほしいものをいれようとしたらインフラエンジニアにお願いしないといけないときだったと思います。
つまり、OSのユーザランド(/usr/bin
とか/usr/lib
とかもろもろのOSのシステムファイル群)を丸ごと固めてコンテナとして実行することで、ホスト側のパッケージと衝突しないことと、アプリケーションエンジニアが作ったイメージをそのまま本番で動かせることが重要だということがわかりました。
Docker + chroot のアイデア
ここまでくると、Dockerでイメージを作るまではよいけど、本番サーバで無理してDockerを使わなくても、自分の用途に沿ったもっとシンプルなコンテナがあるのではないかと思いました。 最初は、rocketやsystemd-nspawnなどをみていましたが、どちらもそれなりに重たい印象でした。 もっとシンプルなコンテナ(的な)ツールとして、kazuhoさんのjailingやvirtualdなどがあります。 まぁつまり実体はchrootです。 chrootはDockerが利用しているLinuxコンテナとは当然別物です。Linuxコンテナを使えば様々なOSのリソースを分離することができます。ただ、別にホスティング事業をやっているわけでもないので、ホストの仮想化のためのプロセス分離もネットワーク分離も自分の用途にはいらないどころかかえって邪魔だということがわかってきました。
Docker + chrootのアイデアの核は非常に単純で以下のようなコマンドで表現できます。
$ docker pull mysql $ export CONTAINER_ID=$(docker create mysql) $ docker export $CONTAINER_ID -o mysql.tar (mysql.tar をMySQLを動かしたいホストへコピーする。) $ tar xvfz /var/containers/mysql/mysql.tar $ sudo chroot /var/containers/mysql mysqld
docker export
により、コンテナのファイルシステムの/
をtarで固めた状態のイメージ(厳密にはDockerイメージとは呼ばない気もしますが、以降ではこれをDockerイメージと呼ぶことにします。)を抽出し、リモートで展開して、展開先のディレクトリでchrootするだけです。
chroot 部分はdocker run
に相当します。
docker run
に対するchroot
のメリットはシンプルな分だけ「既存の運用フローに組み込みやすい」ことです。
例えば、PerlのデプロイにはServer::Starterのようなsupervisor型のホットデプロイツールがよく利用されますが、supervisor的なプロセスの下にアプリケーションのプロセスがぶら下がる形になるので、そもそもDockerのようなdockerデーモンプロセスに各コンテナがぶら下がる形とは相性が悪いと考えます。
chroot(1)は自分のファイルパスの探索ポイントを変更して、引数のコマンドをexecするだけなのでプロセスツリーを崩しません。
単にstart_serverへの引数にchrootコマンドを指定すればよいだけです。
daemontools/supervisorの利用も今までと同様のはずですし、ログは単にchroot先のディレクトリを見にいけばよいし、なによりデプロイ時にアプリケーションサーバの前段のロードバランサで新コンテナ群に振り分け先を切り替えるといった新しい仕組みの導入がいりません。
あとはdocker export
でとりだしたイメージをどのようにして管理し、本番に配布するかです。
Dockerとは直接関係ないですが、次世代デプロイ手法として、Mamiya や Stretcher に代表されるようなアプリケーション成果物をイメージ化し、そのイメージをpull型でデプロイするという手法が昨年あたりから注目されています。(ここでいうイメージはDockerイメージのことではなく、Perlならソースコード+依存するCPANモジュール+静的ファイルなどをtar.gzに固めたものを指します)
これらのフローのうち、アプリケーション成果物をイメージ化する部分をDockerイメージに置き換えるのがよいと考えました。
つまり上記のmysql.tar
(実際にはgzip化します)をCIなどでS3のようなストレージにアップして、本番サーバ上にConsulやCapistranoで配置し、アプリケーションの起動はchrootで行うというような流れです。
とはいえ、chrootするだけとはいっても、chrootするときはホスト側の/sys
、/dev
、/etc/hosts
、/etc/resolv.conf
などのシステムファイルをchroot環境から見えるようにしたいことも多いですし、chrootの実行にはroot権限が必要なので、rootのまま実行せずにLinuxのcapabilitiesを調整するといった下ごしらえが必要です。
さらに、ちょっとしたイメージをデプロイするのに自分の手でS3にアップロードしたりS3からダウンロードするのもちょっと面倒だなと感じます。
droot: Dockerイメージにchrootするコンテナツール
そこで、Dockerイメージとchrootによる一連のデプロイフローをサポートするためのツール drootを作りました。
drootの動作概要を次の図に示しています。
droot
はコマンドラインツールであり、push
、pull
、run
の3つのサブコマンドが基本となります。
それぞれのサブコマンドの機能は、ちょうどdocker
コマンドのそれと近いイメージをもってもらうとよいかもしれません。
以下ではdroot の使い方と実装を紹介します。
droot の使い方
droot push
: Dockerイメージをtar ball化しS3にpushする
$ docker pul yuuk1:perl:5.20.1 $ droot push --to s3://droot-examples/perl.tar.gz perl:5.20.1
mysqlのコンテナをDockerHubからもってきてpushしています。
docker build
でビルドした自前のDockerイメージでももちろん動作します。
droot pull
: S3にpushしたイメージをダウンロードし展開する
MySQLを動かしたいサーバ上で下記のコマンドを実行します。
$ droot pull --dest /var/containers/perl --src s3://droot-examples/perl.tar.gz
droot run
: 展開先のディレクトリにchrootする
$ droot run --root /var/containers/perl perl -v
これら一連のコマンドをすべて使う必要はありません。
他のデプロイツール、例えばstretcherと組み合わせるときは、droot push
でS3にイメージをpushし、イメージの配布はstretcherにまかせてアプリケーション実行時にdroot run
を叩きます。
S3を使っていますが、今のところイメージファイルを1つ1つ素朴に管理しているだけです。 もう少しバージョニングのようなイメージを抽象管理できるような仕組みを持たせてもよいかもしれません。
その他、詳しくはREADMEを参照してください。 まだ環境によっては動作しないオプションなどがあるかもしれませんが、順次対応していきます。(DockerイメージのディストリがDebian 8の場合、setuid/setgid周りの問題で --user/--group オプションが動かない問題など)
droot の実装
drootは各サーバに配る必要があるため、ワンバイナリを生成できるGo言語で実装しました。
droot push/pull の実装
droot push/pull
についてですが、それほど特別なことはしていません。docker exportやS3へのアップロード、gzip化などをUNIXパイプを用いてストリームとして扱い効率化したことと、AWS SDKのGo実装がついにGeneral Releaseされた のでそれを使ったぐらいです。
droot run の実装
jailingを参考したり真似したりしています。 jailingのREADMEにありますが、jailingがやっているのは基本的に以下のようなものだと認識しています。
/bin
、/lib
、/sbin
などのシステムディレクトリをjail環境から参照できるように、それらのディレクトリをchrootディレクトリ以下にbind mountする- chroot ディレクトリ以下にmknodで
/dev/zero
、/dev/random
などを作成 /etc/hosts
,/etc/resolv.conf
などを chroot ディレクトリ以下にコピー- root権限でchroot(2) したのち、root権限の一部だけ残して、権限を落とせるものはすべて落とす。(capabilities
droot run
の実装はこれに近いものになっていますが、違いは実装の違いというよりは目的の違いにあります。
chroot先のディレクトリはLinuxディストリが動作するために必要なファイル/ディレクトリ群が配置されている必要があります。それらのファイル群をホスト側のものを再利用することで、jailingは即座にjail環境を作成できます。
一方で、droot run
はこれらのファイル/ディレクトリ一式はイメージ内に含まれて配布されている前提なので、/bin
や/lib
はホスト側のものを参照する必要はありません。
ただし、/etc/resolv.conf
のような本番サーバとそれ以外で異なる設定にしたいファイルもあるので、本番サーバではホスト側の/eyc/resolve.conf
を参照したいということもあります。そのようなファイルはオプションでコピーしたり、bind mountできるようにしています。
あわせて読みたい
- Docker と SO_REUSEPORT を組み合わせてみる。おそらくその1 - blog.nomadscafe.jp
- 最もおもしろいと思ったDocker本番導入の工夫です。
- tcnksm/awesome-container · GitHub
- コンテナ技術一覧
- Namespaces in operation, part 1: namespaces overview [LWN.net]
- いまさら聞けない Linux コンテナの基礎 / KOF 2015 // Speaker Deck
- Open Container Project
- Kazuho's Weblog: jailing - chroot jailを構築・運用するためのスクリプトを書いた
- chroot 周りの技術はjailingですべて学びました
- Virtual Services Howto: Virtuald
- chrootを使ったシンプルなコンテナ
- Docker without containers: Introducing Pullcontainer (Docker and CVFS)
- drootに近いなにか
- Docker Without Docker
あとがき
Dockerのコンセプト「Build, Ship, Run」に立ち返り、dependency hellを解決するために、CIやstaging環境で動作した環境をイメージ化し、そのまま本番環境にもっていくリーズナブルなやり方を探していました。 結果として、「Build」のみDockerを使用し、Ship、Runは別のシンプルなツールに任せるという方法を自作ツールとともに提案しました。
近いうちに本番環境でのデプロイの具体的な様子を紹介したいと思います。
"Dockerはもう古い これからはchroot"という話をしました
— ゆううき (@y_uuk1) 2015, 6月 27
はてなでは、地に足をつけて、モダンな技術も伝統的な技術も取り入れて、シンプルに課題を解決したいエンジニアを募集しています。
- 作者: Mike Gancarz,芳尾桂
- 出版社/メーカー: オーム社
- 発売日: 2001/02
- メディア: 単行本
- 購入: 40人 クリック: 498回
- この商品を含むブログ (141件) を見る