😎

RancherOSはどのようにpid1をdockerにしたのか

2024/05/04に公開

すでにパブリックアーカイブとなったRancherOSですが、pid1がdockerとなっている特殊なLinuxディストリビューションです。これがどのようにpid1をdockerにしているのかをコードを読みつつ紐解いていきたいと思います。

最初に

Linuxディストリビューションのinitプロセスは最近だとsystemdであったり、一昔前だとsystemvbusyboxでした。これらのinitプログラムの場合はinitスクリプトやunitファイルに書いてある通りに特殊ファイルシステムのマウントやネットワークの設定、デーモンプロセスの起動を行っていました。
しかし、dockerの場合はこれらのことができないはずです。つまり、initプログラムとしてやらなければならないことができないということです。

RancherOSのコードを読む

以下のリンクがRancherOSのコードです。
https://github.com/rancher/os

ざっと眺めてみた感じ以下のリンクにinitプログラムらしきものがあります。
https://github.com/rancher/os/tree/master/cmd/init

ざっくり読んでみたところ以下のような流れで起動しているようです。
いきなりdockerが起動しているわけではなさそうです。

  1. 初期RAMディスクから起動
  2. ルートファイルシステムのセットアップ
  3. ネットワークなどのセットアップ
  4. dockerの起動

このリンクの44行目を読むといくつかのinitプログラムがするべき操作を持っていることがわかります。
https://github.com/rancher/os/blob/master/cmd/init/init.go#L44

81行目にdfs.LaunchDocker()とあるのでこのメソッドの中身を見ていきます。
https://github.com/rancher/os/blob/master/cmd/init/init.go#L81

dfs.LaunchDocker()のコードは以下のリンクにあります。
https://github.com/rancher/os/blob/master/pkg/dfs/scratch.go#L700

初期RAMディスク

初期RAMディスクからマシンにあるディスクに移行するところは以下のところになるかと思います。
https://github.com/rancher/os/blob/master/cmd/init/init.go#L71

ここでは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にしているようです。
https://github.com/rancher/os/blob/master/pkg/dfs/scratch.go#L216

最後に

初期RAMディスクがマウントされている状態で何が起きているのかがわからずもやもやしています。カーネルモジュールの読み込みなど普通の使用用途以外にも何かしているのではと思いますが...

あとはデーモンプロセスにあたるコンテナの起動もどこで行っているのかがこれだけだとわからないですね。

Discussion