Kubernetes 解体新書Part 3 ランタイム

コンテナの正体

アプリ用のファイルを用意し、見える範囲と使える量を設定する。コンテナの起動を Linux の機能に分けて追います。

  • L0 ブラウザだけ
  • 更新 2026年10月6日

コンテナの中で /etc を開くと、ホストとは別のファイルが見えます。 プロセス一覧も分かれているのに、使っているカーネルはホストと同じです。 Part 1 で見たこの状態を、Linux はどう作っているのでしょうか。

この章では、アプリ用のファイルを用意し、見える範囲と使える量を設定してから起動するまでを追います。 次の章で手作りするコンテナの、準備にあたる章です。

GOAL この章のゴール

  • コンテナの起動準備を、Linux の機能に分けて説明できる
  • コンテナのルートとホストのルートが別に扱われる理由が分かる
  • ファイルの変更が書き込み層に保存される仕組みが分かる

chroot から足りないもの

出発点は chroot です。 chroot は、呼んだプロセスの / を別のディレクトリに付け替えるシステムコールです。 ホスト上のディレクトリ (たとえば /srv/ubuntu) に Ubuntu や Alpine のファイル一式を置いて chroot すれば、その中のプロセスは別の OS の上で動いているように振る舞います。 コンテナはこの延長線上にあります。

ただし chroot が付け替えるのは / だけで、プロセス一覧、ネットワーク、hostname はホストと共有されたままです。 chroot した中で ps を打てばホストの全プロセスが見え、ip addr を打てばホストの NIC が見えます。 docker exec で入ったコンテナの中の ps に、ホストのプロセスが出てこなかったのは、これと違うことが起きているからです。

それは、プロセスに見せる一覧 (プロセス、NIC、マウント、hostname など) を、プロセスのグループごとに別々にする Linux カーネルの機能です。 この機能で作った区切りの中では、ps には同じ区切りの中にいるプロセスだけが出ます。 この機能を namespaceネームスペースプロセス一覧、NIC 一覧、マウント一覧などをプロセスのグループごとに別々に持たせる、Linux カーネルの機能。mnt / pid / net / uts / ipc / user / cgroup / time の 8 種類。用語集で見る と呼びます。

もう 1 つ足りないものがあります。 chroot と namespace で区切っても、中のプロセスがホストのメモリを食い尽くすのは止められません。 docker run --memory 256m が効くのは、使える量を決める別の機能があるからです。 CPU、メモリ、プロセス数などの上限を、プロセスの集まりに付けるカーネルの機能が cgroupsシーグループスcontrol group。プロセスの集合に CPU / メモリ / PID 数などの上限を付ける仕組み。v2 では /sys/fs/cgroup の 1 本のツリー。用語集で見る です。 chroot (正確には後述の pivot_root) に namespace と cgroup を足したものが、Linux のコンテナです。

5 ステップ

コンテナができるまでの 5 ステップ: rootfs の用意、namespace、cgroup、pivot_root、execve1. containerd がイメージの層を OverlayFS で mount して rootfs を用意する。2. runc が clone で mnt / pid / net / uts / ipc / cgroup の namespace を新しく作る。3. cgroup のディレクトリに上限を書き、cgroup.procs に PID を入れる。4. 新しい mount namespace の中で pivot_root して root mount を rootfs に入れ替え、古い root を umount する。5. execve でコンテナのプロセスを起動する。1. rootfs を用意層を overlay で mountcontainerd2. namespace見える一覧を分けるrunc3. cgroup使える量を区切るrunc4. pivot_rootroot mount を入れ替えrunc5. execveプロセスを起動runclayer 1layer 2upperdir/tmp/rootfsmntpidnetutsipccgroupuser (任意)memory.max268435456cpu.max50000 100000cgroup.procs← PID を書く/= rootfs (overlay)/oldroot古い root → umountmnt namespace の中でnginx中から見て PID 1= コンテナ1 は containerd の snapshotter が、2〜5 は runc が config.json を読んでやる
図 1コンテナができるまでの 5 ステップ。1 は containerd が、2〜5 は runc がやる。順番と道具はランタイムで違っても、この 5 つは共通。
  1. rootfs を用意する

    コンテナの / になるディレクトリを、rootfsルートエフエスroot filesystem。コンテナのプロセスが / として見るディレクトリ。ホスト上の普通のディレクトリで、中身はイメージから作られる。用語集で見る と呼びます。 Kubernetes のノードでは、containerd がイメージの層を重ねてホスト上に mount し、それを rootfs として runc に渡します。 重ね方は OverlayFS で、次の節で見ます。

  2. namespace を新しく作る

    runc は clone(2) を CLONE_NEWNS (mnt)、CLONE_NEWPID、CLONE_NEWNET、CLONE_NEWUTS、CLONE_NEWIPC、CLONE_NEWCGROUP のフラグ付きで呼び、新しい namespace に入った子プロセスを作ります。 pid_namespaces(7) によると、新しい pid namespace で最初に作られたプロセスが PID 1 になり、その namespace の init として振る舞います。 同じプロセスは、親の pid namespace からは別の番号で見えます。 ネットワークは NIC が lo しか無い空の状態から始まり、あとで veth を差し込みます (Part 2 で手でやった操作です)。

  3. cgroup に入れる

    /sys/fs/cgroup にディレクトリを 1 つ作り、cpu.max と memory.max に上限を書き、プロセスの PID を cgroup.procs に書きます。 Pod の resources.limits は、最終的にこのファイルに書かれた数字になります。 ノードが cgroup v2 かどうかは、stat -fc %T /sys/fs/cgroup/ が cgroup2fs を返すかどうかで分かります。

  4. pivot_root で / を入れ替える

    新しい mount namespace の中で、root mount を rootfs に入れ替えます。 このシステムコールが pivot_rootピボットルート呼んだプロセスの mount namespace の root mount を new_root に入れ替え、古い root mount を put_old の下に移すシステムコール。put_old を umount すると、中から古い root に戻る道が無くなる。用語集で見る です。 仕組みは次の節で 3 段に分けて見ます。

  5. execve でプロセスを起動する

    用意した namespace、cgroup、/ の中で execve します。 Dockerfile の ENTRYPOINT / CMD が実行されるのはここです。 これ以降「コンテナ」として残るのは、このプロセスとその子孫だけです。

Linux の 8 種類の namespace と unshare のフラグmnt / pid / net / uts / ipc / user / cgroup / time。unshare のフラグ 1 つにつき 1 種類を新しく作る。mntunshare -mマウントの一覧pidunshare -p中では PID 1 からnetunshare -nNIC / IP / 経路 / nftutsunshare -uhostnameipcunshare -iSysV IPC / POSIX MQuserunshare -UUID / GID の対応表cgroupunshare -Ccgroup ツリーの根timeunshare -T時計のずれ (5.6+)runc の既定 (cgroup v2 のノード) は user と time 以外の 6 つを新しく作る。user は Pod の hostUsers: false で使う
図 28 種類の namespace。unshare コマンドのフラグが 1 つずつ対応する。次の章で実際に作る。

namespace は種類ごとに独立していて、net だけ新しくして pid は共有する、という組み合わせもできます。 Pod の中のコンテナ同士が同じ IP を持つのは、net namespace を Pod 内で共有しているからです。 一方 pid namespace はコンテナごとに別が既定で、shareProcessNamespace: true を書いたときだけ Pod 内で共有されます。

pivot_root が入れ替えるもの

Part 1 では pivot_root を「chroot と似ていて、元のルートへの参照を切り離せる」と紹介しました。 ここでは、何を何に入れ替えているのかを mount の単位で見ます。 前提になるのは mount namespace の性質です。 mount_namespaces(7) によると、新しい mount namespace を作った直後、その中の mount 一覧は親 (ホスト) の mount 一覧のコピーです。 つまり新しい mount namespace の / は、作った直後はホストの / と同じ root mount を指しています。

pivot_root の 3 段: 新しい mount namespace を作り、root mount を rootfs に入れ替え、古い root を umount する1. 新しい mount namespace では mount 一覧がホストのコピーで、root mount はホストの / のまま、その下に OverlayFS の mount がある。2. pivot_root(new_root, put_old) がこの namespace の root mount を OverlayFS に入れ替え、古い root mount を put_old (/oldroot) の下に移す。3. umount(put_old) で古い root mount を外す。ホスト側の mount namespace はこの間ずっと変わらない。1. 新しい mount namespacemount 一覧はホストのコピー/root mount はホストの //tmp/rootfsoverlay (イメージの層)new_root = /tmp/rootfs2. pivot_root(new_root, put_old)root mount を入れ替える/overlay が root mount に/oldroot古い root mount がここへput_old = /oldroot3. umount(put_old)古い root mount を外す/overlay だけが残る/oldroot (空)ホストへ戻る道が無い入れ替わるのはこの mount namespace の root mount だけ。ホスト側の mount namespace の / は 3 段の間ずっと変わらない。
図 3pivot_root の 3 段。入れ替わるのは新しい mount namespace の root mount だけで、ホスト側の / はずっと変わらない。

1 段目で、新しい mount namespace の中に、containerd が mount した rootfs (OverlayFS) が見えています。 root mount はまだホストの / です。 2 段目の pivot_root(new_root, put_old) が、pivot_root(2) の言葉では「root mount を put_old に移し、new_root を新しい root mount にする」操作です。 rootfs が / になり、古い root mount は rootfs の中のディレクトリ (/oldroot) の下に移ります。 3 段目で /oldroot を umount すると、この mount namespace には rootfs だけが残り、中からホストのファイルシステムへ辿る道が無くなります。

この 3 段の間、ホスト側の mount namespace の root mount はホストの / のままです。 pivot_root が変えるのは、呼んだプロセスが属する mount namespace の root mount だけだからです。 同じ mount namespace にいるプロセスの / は一緒に変わるので、runc は必ず新しい mount namespace を作ってから pivot_root を呼びます。 ホストの mount namespace で呼べば、ホストの全プロセスの / が入れ替わってしまいます。

rootfs を作る OverlayFS

同じ nginx イメージから docker run で 2 つのコンテナを起動し、片方の中で /etc/nginx/nginx.conf を書き換えたとします。 もう片方のコンテナは元の設定のままで、docker pull し直したイメージも変わりません。 書き換えたコンテナを docker rm すると、書き換えも一緒に消えます。 コンテナの / は 1 つのディレクトリに見えるのに、元のファイルは守られ、書き込みはそのコンテナだけに閉じています。

この動きを作っているのが OverlayFSオーバーレイエフエス複数のディレクトリを重ねて 1 つに見せる Linux のファイルシステム。下のディレクトリ (lowerdir) は読み取り専用、書き込みは上のディレクトリ (upperdir) に落ちる。用語集で見る です。 読み取り専用のディレクトリの上に、書き込み用のディレクトリを重ねて、合成した結果を 1 つのディレクトリとして見せます。 mount コマンドで書くと mount -t overlay overlay -o lowerdir=…,upperdir=…,workdir=… merged で、4 つのディレクトリが登場します。

OverlayFS の lowerdir / upperdir / merged読み取り専用のイメージ層 (lowerdir) の上に、コンテナごとの書き込み層 (upperdir) を重ね、合成結果 (merged) をプロセスの / として見せる。書き込みは copy-up で upperdir に落ちる。merged: プロセスが見る /bin/shetc/nginx.conf (変更後)var/log/app.logtmp/old は見えないupperdir: このコンテナだけの書き込み層etc/nginx.conf (コピー)var/log/app.logtmp/old whiteoutlowerdir: イメージの層 (読み取り専用、他のコンテナと共有)layer 2: etc/nginx.conflayer 1: bin/sh tmp/old lib/…copy-upルール読む: 上から探す書く: upper にコピーしてから書く消す: upper にwhiteout を置くlower は変わらない= イメージは不変workdir: カーネルの作業用 (upper と同じ FS に置く)mount -t overlay
図 4OverlayFS の 3 層。プロセスが見るのは merged だけで、書き込みは全部 upperdir に落ちる。
  • lowerdir:下に重ねる読み取り専用のディレクトリ (イメージの中身が入ります)。同じイメージから起動した全コンテナで共有されます。複数指定でき、: 区切りで一番左が一番上になります。
  • upperdir:コンテナごとの書き込み用ディレクトリ。lowerdir にあるファイルを書き換えると、まずそのファイルが upperdir にコピーされ (copy-up)、それから書き込まれます。
  • merged:合成結果。コンテナの rootfs になるのはここです。
  • workdir:カーネルが copy-up の途中で使う作業場所。upperdir と同じファイルシステムに置きます。

ファイルを消す操作も upperdir に落ちます。 upperdir に「このファイルは削除済み」を示す特別なファイル (whiteout。デバイス番号 0/0 のキャラクターデバイス) が作られ、merged からは見えなくなります。 lowerdir は一切変わらないので、イメージは不変です。 コンテナを消せば upperdir ごと消えるので、コンテナの中に書いたファイルは残りません。

イメージの層

lowerdir に入るイメージの中身は、いくつかの塊に分かれています。 docker pull の出力に Pull complete の行が何本も流れるのは、その塊を 1 つずつダウンロードしているからです。 この塊を 層そうイメージを構成する、前の層からの差分 (追加、変更、削除したファイル) だけを tar に固めたもの。多くは gzip か zstd で圧縮される。用語集で見る と呼びます。 OCI Image Spec の言葉では、層は「ファイルシステムの変更セット」を tar にしたもので、1 つずつ順に適用すると最終的なファイルシステムになります。

層は下から順に、OverlayFS の lowerdir として重ねられます。 たとえば下の層に /bin/sh、上の層に /app/server があれば、重ねた結果には両方が並びます。 同じ層は 1 回しかディスクに置かれないので、同じベースイメージから作った 2 つのイメージは、ベース部分の層を共有します。

VM との違い

コンテナと仮想マシンの違い: カーネルを共有するか、ゲストカーネルを持つかコンテナはホストの Linux カーネル 1 つを namespace と cgroup で区切って共有する。VM はハイパーバイザーの上でそれぞれがゲストカーネルを持つ。コンテナ (runc)nginxns + cgroupappns + cgroupdbns + cgroupsyscallsyscallsyscallLinux カーネル (1 つ、全員で共有)ハードウェア仮想マシン (KVM / Firecracker)VM 1appゲストカーネルVM 2dbゲストカーネルホストカーネル + ハイパーバイザー (KVM)ハードウェア (VT-x / AMD-V)
図 5コンテナはホストのカーネルを共有する。VM はゲストカーネルを持つ。

コンテナの中のプロセスがファイルを開くと、openat システムコールがホストの Linux カーネルにそのまま届きます。 カーネルはホストの 1 つしか無く、namespace が変えているのは、カーネルが各プロセスに返す答え (どのプロセス一覧を見せるか、どの NIC を見せるか) だけです。 だから起動は clone と execve の速さで終わり、メモリもプロセス分しか使いません。

同じ理由で、カーネルに脆弱性があれば全コンテナに影響します。 VM でゲストカーネルの脆弱性を突いても、そこはまだ VM の中で、ホストに届くにはハイパーバイザー (VM を動かすホスト側のプログラム) の穴をもう一段突く必要があります。 コンテナにはこの一段がありません。 この一点が、この Part の後半で扱う gVisor や Kata Containers の存在理由です。

ふりかえり

Qrunc が新しい mount namespace の中で pivot_root を呼んだ。ホストの / はどうなる?

pivot_root が入れ替えるのは、呼んだプロセスの mount namespace の root mount です。新しい mount namespace の中で呼ぶので、ホストの mount namespace には何も起きません。

Qコンテナの中でイメージ由来のファイルを書き換えた。どこに書かれる?

copy-up です。lowerdir (イメージ) は決して変わらないので、同じイメージを使う他のコンテナには影響しません。

Qコンテナと VM の違いを 1 つだけ挙げるなら?

カーネルの数が違います。起動の速さも、密度も、分離の弱さも、全部ここから出てきます。

参考