Docker は変だ
著者は遊びでコンテナランタイムを自作し、Docker の奇妙な点を 6 つ挙げている。コンテナは VM でもカーネル機能でもない、イメージはただのフォルダ、runc は Go に fork がないため /proc/self/exe を exec して fork を装う、しかも root が要る。
日本語
コピー
面白半分で、自分でコンテナランタイムを書いてみることにした。まだ初期段階だが、それでも Docker がどれだけ奇妙かはもう思い知らされた。以下は、Docker の奇妙な点について私が知ったことだ。
コンテナは実在しない
仮想マシンと違って、コンテナは特別な制限をかけられたプロセスにすぎない。そのプロセスを動かしているのは、ほかでもないあなたのカーネルだ。どこを探せばいいかさえ分かっていれば、/procの中からこれらのプロセスを見つけることすらできる。
コンテナは本当に実在しない
Linux には実は「コンテナ」という名前の機能も存在しない。コンテナの正体は、5 つの異なる Linux カーネル機能がトレンチコートを着て 1 つになりすましているものだ。
namespace はホストとコンテナがそれぞれ何を見られるかを決め、cgroup はコンテナのリソース使用量を制限し、それに加えて私にはよく分からないネットワークの魔法がいろいろあって、コンテナのネットワークを分離している。
イメージはフォルダだ
イメージはただのフォルダだ。Docker はそれをどこかに置いておき、chroot(またはpivot_root)を呼び出してそのフォルダをコンテナプロセスの新しいルートディレクトリにする。つまりdocker run -it --rm ubuntu /bin/bashを実行すると、Docker はこうする。
- Dockerhub から ubuntu イメージをダウンロードする。
- そのイメージをどこかに置く。
/some/weird/docker/path/bin/bashを実行する
Go はコンテナランタイムにとってかなりひどい言語だ
Docker のコンテナランタイムであるruncは Go で書かれているが、Go の設計上の選択のいくつかが、コンテナランタイムにはあまり向いていない。Go のランタイムは生まれつきマルチスレッドだ。クールな特徴だが、runcの中では厄介ごとを引き起こす。
コンテナランタイムが発行する必要のある Linux システムコールの多くは、シングルスレッドの環境で実行しなければならない。だからruncには狂った C モジュールがあって、これが Go ランタイムの初期化より前に動くことで、これらのシステムコールを発行できるようにしている。
Go には fork もない!fork はメインスレッドしかコピーしないので、Go ランタイムのような環境では自分を傷つけかねない落とし穴だ。しかしコンテナランタイムは、fork して制限を山ほど設定し、docker runに実際に渡されたプロセスを exec したい。これを回避するため、runcは/proc/self/exeを特別なコマンドライン引数付きで exec し、子プロセスであることを示すことで、fork を近似的に実現している。
Docker には root 権限が必要だ!
macOS で Docker を動かしている私のような人間には、この問題は見落としやすい。コンテナランタイムが使う Linux カーネル API の多くは(少なくとも以前は)root としての実行を要求するので、Docker デーモンは通常 root で動いている。コンテナのセキュリティを確保するのがどれだけ難しいか、想像するしかない。
今はかなりマシになっているようだ。最大の例外はネットワークだ。rootless のコンテナランタイムを使う場合、同じ機能を本当に提供するには、ユーザー空間でネットワークスタック全体を再実装する必要があるらしい。
学ぶのは楽しい!
自分のコンテナランタイムはまだ始まったばかりなのに、もう Docker についてずいぶん多くのことを知った気がする。自分が使っているツールを自分で書き直してみるのは、それを理解するための良い方法だ。