RancherOSはどのようにpid1をdockerにしたのか
すでにパブリックアーカイブとなったRancherOSですが、pid1がdockerとなっている特殊なLinuxディストリビューションです。これがどのようにpid1をdockerにしているのかをコードを読みつつ紐解いていきたいと思います。
最初に
Linuxディストリビューションのinitプロセスは最近だとsystemd
であったり、一昔前だとsystemv
やbusybox
でした。これらのinitプログラムの場合はinitスクリプトやunitファイルに書いてある通りに特殊ファイルシステムのマウントやネットワークの設定、デーモンプロセスの起動を行っていました。
しかし、dockerの場合はこれらのことができないはずです。つまり、initプログラムとしてやらなければならないことができないということです。
RancherOSのコードを読む
以下のリンクがRancherOSのコードです。
ざっと眺めてみた感じ以下のリンクにinitプログラムらしきものがあります。
ざっくり読んでみたところ以下のような流れで起動しているようです。
いきなりdockerが起動しているわけではなさそうです。
- 初期RAMディスクから起動
- ルートファイルシステムのセットアップ
- ネットワークなどのセットアップ
- dockerの起動
このリンクの44行目を読むといくつかのinitプログラムがするべき操作を持っていることがわかります。
81行目にdfs.LaunchDocker()
とあるのでこのメソッドの中身を見ていきます。
dfs.LaunchDocker()
のコードは以下のリンクにあります。
初期RAMディスク
初期RAMディスクからマシンにあるディスクに移行するところは以下のところになるかと思います。
ここではinitFuncs
に登録されている関数を実行するようになっています。初期RAMディスクがルートファイルシステムになっている状態からマシンに接続されているディスクへの移行をここで行っているのかと思います。
ルートファイルシステム
PrepareFS()
PrepareFs()
という関数があります。この関数でルートファイルシステムのセットアップを行っていると考えられます。ここで、ディレクトリの作成やproc
などの特殊ファイルシステムをマウントしたりなど行っているのでしょう。
runOrExec()
この関数をを読むとsecondPrepare()
とありルートファイルシステム以外のLinuxシステムのセットアップを行っていることが予想できます。
この関数の中身を読むとsetupNetworking()
とあり、ネットワークに接続するための設定を行っています。次のtouchSockets()
ではdocker.sockの準備をしているようです。
ここで一度、RancherOSの大雑把なアーキテクチャの話に一度移ります。
画像元:https://rancher.com/docs/os/v1.x/en/
先ほどまでpid1がdockerと言っていましたが、そのdockerはこの画像でいうところのsystem dockerにあたるものです。RancherOSでは、コンソールからdockerを操作する場合はuser dockerの方を利用することになりますが、ホスト側との通信が必要なケースがあるということなのではないかと思います。
※筆者はRancherOSを使い込んだことがなく、なぜdocker.sock
を用意しているかの具体的な理由はわかりません。このあたり知っているかたいましたらコメントしていただけると幸いです。
コードリーディングの方に戻ります。先ほどはtouchSockets()
まで読んだかと思います。setupLogging()
はロガーのセットアップだと予想してますが、どちらにせよ今回の本旨であるRancherOSはどのようにpid1をdockerにしたのかからは反れるので飛ばします。
続いてsetupBin()
です。この関数ではどうやらコマンドのセットアップをしていることが予想されます。わざわざこんなことをしているのはinitプログラム起動時点ではPATHが通っていないため、このinitプログラムから扱うためにあるディレクトリにPATHを通し、そのディレクトリにシンボリックリンクを張っています。(全部絶対パスだとpid1がdockerに変わったときにdockerからiptablesなどのコマンドが見つからなくて起動しなかったりするのでしょうか?)
次にsetUlimit()
を読みます。これは一度に開けるファイルの上限数を設定しているところになるかと思います。
ここまでで、ルートファイルシステムのセットアップとLinuxシステムのセットアップが完了した状態になります。ここまでやった後にexecDocker()
を起動します。
execDocker()
syscall.Exec()
でdockerを起動しています。この関数は子プロセスを立ち上げるのではなくプロセスが置き換わります。Go言語の使用をうまく利用してpid1をdockerにしているようです。
最後に
初期RAMディスクがマウントされている状態で何が起きているのかがわからずもやもやしています。カーネルモジュールの読み込みなど普通の使用用途以外にも何かしているのではと思いますが...
あとはデーモンプロセスにあたるコンテナの起動もどこで行っているのかがこれだけだとわからないですね。
Discussion