読者です 読者をやめる 読者になる 読者になる

Dockerとchrootを組み合わせたシンプルなコンテナデプロイツール

この記事ははてなエンジニアアドベントカレンダー2015の1日目です。今回は、既存の運用フローに乗せやすいDockerイメージへのchrootによるデプロイの考え方と自作のコンセプトツール droot を紹介します。

github.com

背景

Dockerがリリースされてから3年近く経過しました。 Web界隈において、確か初期には、テスト環境をすばやく作れて便利な高速に動くVagrantのようなものという扱いだったと思います。そこから、ローカルやCIで作成したコンテナをイメージ化し、本番環境までもっていけるというポータビリティの高さが注目されました。 とはいえ、実際にDockerコンテナを本番環境、特にアプリケーションサーバとして動作させるためには、いくつもの課題があります。 2年前はまだいつか本番に投入するぞぐらいの気持ちでした。当時のブログエントリの様子です。

しかし、Docker自体がリリースを重ねて機能が増えて、動作が安定してきた結果、本番環境での利用が現実的になってきているかもしれません。 実際に、本番環境に導入された事例もいくつか見聞きしています。

Docker 本番導入の課題

変更があればコンテナを捨てて新しいコンテナを立ち上げるImmutable Infrastructure要素と、1度作成したホストを使いまわしていく既存の運用フローと、どうすり合わせるかがDocker導入の壁です。 少なくとも僕が課題だと認識しているもしくはしていたのは以下のようなものです。(他にもあった気がしますがとりあえず今思い出せる範囲で)

  • ChefやAnsibleとの兼ね合い
  • Dockerイメージの管理
  • Dockerコンテナの監視
  • コンテナのゴミ掃除
  • Server::Starter動かないんでは問題
  • ネットワークまわりのパフォーマンス劣化(パフォーマンスの観点からみるDockerの仕組みと性能検証 #dockerjp - ゆううきブログ)、docker
  • docker コンテナ内の問題調査の手間
  • dockerコンテナ内のログの取り扱い
  • docker pull が遅い
  • storage driverがdevice mapperのとき、docker builddocker 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さんのjailingvirtualdなどがあります。 まぁつまり実体は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とは直接関係ないですが、次世代デプロイ手法として、MamiyaStretcher に代表されるようなアプリケーション成果物をイメージ化し、そのイメージを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はコマンドラインツールであり、pushpullrun の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

perlのコンテナをDockerHubからもってきてpushしています。 docker buildでビルドした自前のDockerイメージでももちろん動作します。

droot pull: S3にpushしたイメージをダウンロードし展開する

Perlを動かしたいサーバ上で下記のコマンドを実行します。

$ 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のコンセプト「Build, Ship, Run」に立ち返り、dependency hellを解決するために、CIやstaging環境で動作した環境をイメージ化し、そのまま本番環境にもっていくリーズナブルなやり方を探していました。 結果として、「Build」のみDockerを使用し、Ship、Runは別のシンプルなツールに任せるという方法を自作ツールとともに提案しました。

近いうちに本番環境でのデプロイの具体的な様子を紹介したいと思います。

はてなでは、地に足をつけて、モダンな技術も伝統的な技術も取り入れて、シンプルに課題を解決したいエンジニアを募集しています。

はてなの2017年度 新卒採用サイトがオープンしました。 developer.hatenastaff.com

kazuho さんから最高のコメント?いただきました。SIGHUP契機でディレクトリにイメージ展開もやるのかなるほど。