コンテナを証明する四つのファイル
一言でいうと
カーネルに「コンテナ」というデータ構造はありません。代わりに、プロセスごとに/proc/<pid>/の下に、ネームスペース・cgroup・マウント・権限が書かれていて、その4つのファイルを読めば、そのプロセスがどんな分離の中にいるのかがすべて明らかになります。
なぜ必要なのか
「このプロセスはコンテナの中にありますか」という質問にDockerのコマンドで答えるには、Dockerが必要です。ところが、障害調査で出会う状況は、たいてい逆です。ホストでpsを打ってみると、見慣れないプロセスがCPUを食っていて、これがどのコンテナのもので、何が制限されているかを知る必要があります。
/procは、Docker・containerd・podmanのどれを使っていても、同じように答えてくれます。ランタイムではなく、カーネルに直接尋ねているからです。
ファイル1(/proc/PID/ns/): 何を見られるか
$ ls -l /proc/1/ns/
mnt -> 'mnt:[4026532187]'
net -> 'net:[4026532190]'
pid -> 'pid:[4026532188]'
角括弧の中の数字がinode番号で、これが同じなら同じネームスペースです。ホストの/proc/1/ns/netと比較して違えばネットワークが分離されていて、同じなら--network hostで起動したものです。2つのコンテナを比較すれば、どのネームスペースを共有しているかもわかります。
ファイル2(/proc/PID/cgroup): どれだけ使えるか
0::/kubepods.slice/kubepods-burstable.slice/.../cri-containerd-9f3c1a....scope
cgroup v2では1行です。この経路にコンテナIDが入っています。ホストで見慣れないプロセスの正体を明らかにするとき、最も速い道です。そして、この経路を/sys/fs/cgroup/の下でたどれば、実際の制限値を読めます。
/sys/fs/cgroup/<경로>/memory.max # 메모리 상한
/sys/fs/cgroup/<경로>/memory.current # 현재 사용량
/sys/fs/cgroup/<경로>/cpu.max # "200000 100000" = 2 코어
このコードブロックの韓国語コメントは、順に、メモリの上限、現在の使用量、「200000 100000」は2コアを意味する、という意味です。
ファイル3(/proc/PID/mountinfo): ファイルがどこから来るか
コンテナのルートファイルシステムは、たいていoverlayです。
... / overlay rw,lowerdir=/var/lib/.../l/ABC:/var/lib/.../l/DEF,upperdir=...
lowerdirにコロンでつながれた一覧がイメージのレイヤーで、upperdirがコンテナの書き込みレイヤーです。ここでボリュームのマウントも一緒に見えます。どのホストのパスがどこに付いているかを確認するときに使います。
ファイル4(/proc/PID/status): 何をできるか
CapEff: 00000000a80425fb
NoNewPrivs: 1
Seccomp: 2
CapEffは実効capabilityのビットマスクです。0なら特権が何もないという意味で、0000003fffffffffならほぼすべてを持っていることで、--privilegedの痕跡です。NoNewPrivs: 1はallowPrivilegeEscalation: falseの結果で、Seccomp: 2は、seccompフィルターがかかっているという意味です。
capsh --decode=00000000a80425fbで、人が読める形に展開できます。
まとめ: 4つのファイルが答える質問
| ファイル | 答える質問 |
|---|---|
ns/ |
何を見られるか(PID・ネットワーク・マウントの分離) |
cgroup |
何をどれだけ使えるか + どのコンテナか |
mountinfo |
ファイルがどこから来るか(レイヤー・ボリューム) |
status |
何をできるか(capability・seccomp) |
同じファイルで答えられる実際の質問
/procの読み方を学ぶと、ツールのない環境でも答えられる質問が増えます。よく使う4つをまとめておきます。
「この2つのコンテナはネットワークを共有しているか」: 名前空間のinode番号を比較します。同じなら同じ名前空間です。
readlink /proc/$A/ns/net /proc/$B/ns/net
# net:[4026532567] 이 같으면 같은 네트워크
サイドカーが本体のネットワークを見られない問題や、一時コンテナがプロセスを見られない問題が、すべてこの1行で確認できます。
「メモリの上限は本当にかかっているか」: cgroup v2では、経路をたどってファイルを読みます。
cat /proc/$PID/cgroup # 0::/kubepods/.../<id>
cat /sys/fs/cgroup/kubepods/.../memory.max # 한도
cat /sys/fs/cgroup/kubepods/.../memory.current
cat /sys/fs/cgroup/kubepods/.../memory.events # oom_kill 횟수가 여기 있다
memory.eventsのoom_killが0でなければ、そのコンテナはすでに1回死んでいます。Podが再起動した理由を探すとき、最初に見る場所です。
「このファイルはどこから来たのか」: mountinfoを見ると、ボリュームが実際にどこに付いていて、読み取り専用かどうかがわかります。ConfigMapが更新されない問題(シンボリックリンクでマウントされる構造)が、ここで明らかになります。
grep -E ' /etc/config | /data ' /proc/$PID/mountinfo
「このプロセスは何をできるのか」: statusのCapEffを、人が読める形に展開します。
grep CapEff /proc/$PID/status
capsh --decode=00000000a80425fb
CapEffが0000000000000000なら、権限をすべて捨てたということで、それが私たちの望む状態です。逆に0000003fffffffffが見えれば、そのコンテナは事実上、特権モードで動いています。
ホストからコンテナを探す逆方向もよく使います。ホストでCPUを多く使うプロセスを見つけたとき、それがどのPodのものかは、cgroupのパスに書かれています。
現場での姿
- ホストのCPUを食っているプロセスのコンテナを探すとき:
cgroupの1行で終わりです。 - OOMKilledなのに
docker statsでは余裕がありそうに見えるとき:memory.maxとmemory.currentを直接比較します。統計はサンプリングなので、瞬間的な急増を見逃します。 - 「権限を最小化した」というイメージの検証:
CapEffが0かを確認します。ドキュメントではなく、カーネルが答えます。
次の確認で見ること
続くクイズでは、/procのネームスペースのinode、cgroupの経路と制限、capabilityのビットマスクが、それぞれ何を証明するかを確認します。前のラボで読んだカーネルの値と、ドキュメント上のコンテナ設定を区別して、答えてみてください。