TT Lab
はじめる
学ぶ 学習パス コース

コンテナの内部原理

名前空間を目で確かめる

TT Labで続きを見る

このラボは本物のVM上で動きます

この箱はPodではなく、KubeVirtが起動した仮想マシンです。Linuxカーネルが別に動き、systemdが実際にサービスを管理し、dockerは模倣ではなく本物のDockerエンジンです。docker runで起動したコンテナは実際にプロセスになり、docker execもdocker logsもそのまま動作します。

以前は、このラボはPodの中で動いていました。カーネルの権限をすべて下ろした箱なので、コンテナを起動するステップが塞がれていて、そのためイメージのアーカイブを直接展開してみるという回り道で学んでいました。もう回り道は必要ありません。

知っておくことが2つあります。

目標

7種類のネームスペースのうち、pid、uts、mnt、net、userの5つを自分で測り、コンテナが「箱」ではなく見える範囲が制限された普通のプロセスであることを、inode番号で証明します。最後に、2つのコンテナがネットワークネームスペースを共有するKubernetesのPodの構造を、Dockerだけで再現します。

なぜ重要なのか

コンテナのトラブルシューティングが行き詰まる場所は、ほとんどが「これは分離されているのか、いないのか」がわからないことから生じます。ポートがなぜ衝突するのか、コンテナの中で見たファイルがホストのどこにあるのか、サイドカーがなぜlocalhostでアプリに接続できるのか。答えはすべて、「そのリソースがどのネームスペースに属するか」の一言です。ネームスペースにはinode番号が付き、/proc/<pid>/ns/*で読めるので(プレースホルダーはpidです)、この問いは推測ではなく測定で答えられます。同じ番号なら同じネームスペース、違う番号なら違うネームスペース。それがすべてです。

ステップ

  1. /root/int1ディレクトリを作成し、ホストで/proc/self/ns/pidのリンクが指す値を、/root/int1/host-pid-ns.txtに保存します。ファイルの内容は、pid:[숫자]の1行である必要があります(プレースホルダーは数字です)。
  2. alpine:3.20イメージで、dk-nsという名前のコンテナを、--hostname labhub-utsを付けてsleep infinityでバックグラウンド実行し、そのコンテナの中で読んだ/proc/self/ns/pidの値を/root/int1/ctr-pid-ns.txtに保存します。ステップ1の値と違う必要があります。
  3. dk-nsの中で、プロセス一覧をPID列が見える形式で取り出して、/root/int1/pid1.txtに保存します(docker exec dk-ns ps -o pid,comm)。PID 1はsleep、つまりそのコンテナを起動するときに渡したコマンドである必要があります。systemdでもinitでもないという点が要点です。このVMで、ただps -eを実行すると、PID 1はsystemdなので、対比になります。
  4. dk-nsのホスト名を、/root/int1/uts.txtに保存します。内容は、labhub-utsの1行である必要があります。
  5. このVMの/etc/os-releaseと、alpineイメージ側の/etc/os-releaseを1つのファイルに続けて/root/int1/mnt-proof.txtに保存します。ファイルの中に、ホスト側のubuntuとコンテナ側のalpineが、両方見える必要があります。 docker run --rm alpine:3.20 cat /etc/os-releaseと、このVMの同じファイルを、1つのファイルに連結すればよいのです。あわせてuname -rも出力してみてください。カーネルは同じで、ファイルツリーだけが違います。マウントネームスペースがしているのは、結局、プロセスの/を別のツリーにすり替えることだけだという事実が、その2行に含まれています。
  6. dk-nsの中で読んだ/proc/net/devを、/root/int1/ctr-net.txtに保存します。lo:の行があり、ホストの同じファイルと内容が違う必要があります。インターフェースの構成と送受信の統計は、ネームスペースごとに別々に管理されます。
  7. dk-nsのネットワークネームスペースに参加するdk-sidecarコンテナをバックグラウンドで起動し、dk-nsの中で読んだ/proc/self/ns/netを/root/int1/ns-a.txtに、dk-sidecarの中で読んだ同じ値を/root/int1/ns-b.txtに保存します。2つの値は、net:[숫자]の形式(プレースホルダーは数字です)で、かつ互いに同じである必要があります。
  8. ホストの/proc/self/uid_mapを/root/int1/host-uidmap.txtに、dk-nsの中の/proc/self/uid_mapを/root/int1/ctr-uidmap.txtに保存します。2つのマッピングの内容が、互いに違う必要があります。

参考

ホストのPIDネームスペースの識別子を読む

/root/int1ディレクトリを作成し、ホストで/proc/self/ns/pidのリンクが指す値を、/root/int1/host-pid-ns.txtに保存します。ファイルの内容は、pid:[숫자]の1行である必要があります(プレースホルダーは数字です)。

ネームスペースの識別子は、/proc/self/ns/の下にシンボリックリンクとして公開されています。リンクが指す文字列そのものが答えなので、catではなく、リンクをたどるコマンドを使う必要があります。ファイルには、pid:[숫자]の1行だけが残る必要があります(プレースホルダーは数字です)。

コンテナのPIDネームスペースと比較する

alpine:3.20イメージで、dk-nsという名前のコンテナを、--hostname labhub-utsを付けてsleep infinityでバックグラウンド実行し、そのコンテナの中で読んだ/proc/self/ns/pidの値を/root/int1/ctr-pid-ns.txtに保存します。ステップ1の値と違う必要があります。

同じパスをコンテナの中で読んで初めて、違う番号が出ます。ホストのシェルで読むと、ステップ1とまったく同じ値が出て失敗します。コンテナは次のステップで引き続き使えるよう起動し続けている必要があるので、すぐには終了しないコマンドで起動してください。

コンテナの中のPID 1を確認する

dk-nsの中で、プロセス一覧をPID列が見える形式で取り出して、/root/int1/pid1.txtに保存します(docker exec dk-ns ps -o pid,comm)。PID 1はsleep、つまりそのコンテナを起動するときに渡したコマンドである必要があります。systemdでもinitでもないという点が要点です。このVMで、ただps -eを実行すると、PID 1はsystemdなので、対比になります。

プロセス一覧を取り出しますが、PID列も一緒に見える形式である必要があります。コンテナのメインコマンドが、そのままPID 1のプロセスです。ホストで取り出すと、PID 1は別のプロセスとして出ます。

UTSネームスペースで分離されたホスト名

dk-nsのホスト名を、/root/int1/uts.txtに保存します。内容は、labhub-utsの1行である必要があります。

ホスト名を変更するのではなく、コンテナを作るときに付けたホスト名が、UTSネームスペースのおかげでホストとは別に存在することを確認するステップです。コンテナの中でホスト名を尋ねて、その値だけを保存してください。

マウントネームスペース: 同じカーネル、違うファイルツリー

このVMの/etc/os-releaseと、alpineイメージ側の/etc/os-releaseを1つのファイルに続けて/root/int1/mnt-proof.txtに保存します。ファイルの中に、ホスト側のubuntuとコンテナ側のalpineが、両方見える必要があります。 docker run --rm alpine:3.20 cat /etc/os-releaseと、このVMの同じファイルを、1つのファイルに連結すればよいのです。あわせてuname -rも出力してみてください。カーネルは同じで、ファイルツリーだけが違います。マウントネームスペースがしているのは、結局、プロセスの/を別のツリーにすり替えることだけだという事実が、その2行に含まれています。

片方だけを入れると、比較になりません。ホストで読んだものと、コンテナの中で読んだものを、同じファイルに続けて残してください。連結するときは、上書きではなく、追記のリダイレクトを使います。

ネットワークネームスペースのインターフェース一覧

dk-nsの中で読んだ/proc/net/devを、/root/int1/ctr-net.txtに保存します。lo:の行があり、ホストの同じファイルと内容が違う必要があります。インターフェースの構成と送受信の統計は、ネームスペースごとに別々に管理されます。

インターフェース一覧は、/proc/net/devでも読めます(この環境ではip/ifconfigより確実です)。コンテナの中で読むと、loとコンテナのインターフェースくらいしか見えず、ホストより行数が少ないはずです。

2つのコンテナにネームスペースを共有させる

dk-nsのネットワークネームスペースに参加するdk-sidecarコンテナをバックグラウンドで起動し、dk-nsの中で読んだ/proc/self/ns/netを/root/int1/ns-a.txtに、dk-sidecarの中で読んだ同じ値を/root/int1/ns-b.txtに保存します。2つの値は、net:[숫자]の形式(プレースホルダーは数字です)で、かつ互いに同じである必要があります。

新しいネットワークネームスペースを作る代わりに、他のネームスペースに参加させるオプションがあります。KubernetesのPodで、pauseコンテナがしていることと同じです。参加が成功していれば、2つのコンテナのnet inode番号が、完全に同じになるはずです。

ユーザーネームスペースのUIDマッピング

ホストの/proc/self/uid_mapを/root/int1/host-uidmap.txtに、dk-nsの中の/proc/self/uid_mapを/root/int1/ctr-uidmap.txtに保存します。2つのマッピングの内容が、互いに違う必要があります。

/proc/self/uid_mapは、컨테이너안UID 호스트UID 개수の3列です(プレースホルダーは、コンテナ内のUID、ホストのUID、個数です)。この環境はrootlessなので、ホスト側のシェルとコンテナの内側でマッピングが違って出ます。読み物の実測表(Dockerのデフォルトではuserネームスペースが同じだった)と、なぜ違うのかを考えてみてください。