Skip to content
Published on

コンテナは嘘である — cgroupsとネームスペースで見るカーネルの真実

Authors

はじめに

挑発的なタイトルからまず釈明します。カーネルのソースコードに「コンテナ」というオブジェクトはありません。struct containerのようなものは存在しないのです。私たちがコンテナと呼ぶものは、ネームスペース(何が見えるか)、cgroups(どれだけ使えるか)、そしてセキュリティ機構(何ができるか)を1つのプロセスに重ねて適用した結果にすぎません。Dockerもcontainerdもruncも、結局はこれらのカーネル機能を組み合わせるオーケストレーターです。

この事実がなぜ重要なのでしょうか。コンテナを「VMの軽量版」という誤ったモデルで捉えていると、コンテナエスケープ脆弱性がなぜ生まれるのか、なぜすべてのコンテナがカーネルを共有するのか、OOM Killがなぜあのように動くのかを理解できません。逆に「カーネル機能の組み合わせ」という正確なモデルを持てば、障害時に抽象化を剥がして/sys/fs/cgroupと/procを直接読み、真実に到達できます。

この記事ではその組み合わせを自分の手で作ってみます。unshareでネームスペースを1つずつ体験し、cgroupファイルを直接操作し、最後にbashとGoでミニコンテナを作ります。

コンテナ = カーネル機能の組み合わせ

まず全体の構図を図にします。

        「コンテナ」という幻想
  +--------------------------------------+
  |  実はただのLinuxプロセス              |
  |                                      |
  |  + ネームスペース (隔離: 何が見えるか) |
  |    pid, net, mnt, uts, ipc,          |
  |    user, cgroup, (time)              |
  |                                      |
  |  + cgroups (制限: どれだけ使えるか)    |
  |    cpu, memory, io, pids ...         |
  |                                      |
  |  + セキュリティ層 (権限: 何ができるか) |
  |    capabilities, seccomp,            |
  |    LSM (SELinux/AppArmor)            |
  |                                      |
  |  + ルートファイルシステム              |
  |    (overlayfsイメージ)               |
  +--------------------------------------+
            |
            v
     全員が1つのホストカーネルを共有
     (VMとの根本的な違い)

核心の命題: コンテナ内のプロセスは、ホストでpsすれば見える平凡なプロセスです。ただ、そのプロセスの「視界」と「上限」と「権限」が操作されているだけです。

ネームスペース7種ツアー — unshare実習

ネームスペースはカーネルリソースの「視界」を分離します。現在安定して使われるのは8種(timeを含む)ですが、コンテナの核心は7種です。それぞれunshareコマンドで直接体験してみます。

ネームスペース隔離対象コンテナでの役割
pidプロセスID空間コンテナ内でPID 1から始まる
netネットワークスタック全体コンテナごとのIF/ルーティング/FW
mntマウントポイントコンテナごとのファイルシステムツリー
utsホスト名、ドメイン名コンテナごとのhostname
ipcSystem V IPC、POSIXメッセージキュー共有メモリの隔離
userUID/GIDマッピングrootlessコンテナの核心
cgroupcgroupルートの視界自分のcgroup位置を隠す

1つずつ触ってみます。

# 1) uts: 最も簡単なネームスペース。ホスト名の隔離
sudo unshare --uts bash
hostname container-test   # ホストのhostnameは変わらない
hostname                  # container-test
exit

# 2) pid: PID空間の隔離。--forkと--mount-procが鍵
sudo unshare --pid --fork --mount-proc bash
echo $$                   # 1  <- このシェルがPID 1
ps aux                    # プロセスが2〜3個しか見えない
exit

# 3) net: 空のネットワークスタックができる
sudo unshare --net bash
ip link                   # loが1つだけ、しかもDOWN状態
exit

# 4) mnt: マウントツリーの隔離
sudo unshare --mount bash
mount -t tmpfs tmpfs /mnt # ホストには見えないマウント
findmnt /mnt
exit

# 5) ipc: 共有メモリの隔離
ipcmk -M 1024             # ホストで共有メモリ作成
sudo unshare --ipc bash
ipcs -m                   # 見えない (隔離済み)
exit

# 6) user: 非rootユーザーが「ネームスペース内でだけroot」
unshare --user --map-root-user bash   # sudo不要!
id                        # uid=0(root) ... しかし
cat /proc/self/uid_map    # 0 <自分のUID> 1 形式のマッピングを確認
exit

# 7) cgroup: cgroupパスの視界の隔離
sudo unshare --cgroup bash
cat /proc/self/cgroup     # ルート(/)のように見える
exit

すべて結合すれば、すでにコンテナの骨格です。

unshare --user --map-root-user --pid --fork --mount-proc \
        --net --uts --ipc --cgroup bash

userネームスペースとrootlessコンテナ

userネームスペースはセキュリティの観点で最も重要なピースです。鍵はUIDマッピングです。ネームスペース内のUID 0(root)がホストの非特権UID(例: 100000)にマッピングされれば、コンテナ内でrootのように振る舞っても、ホストから見れば平凡なユーザーです。

   コンテナ内の視界              ホストの真実
  +----------------+          +---------------------+
  | uid 0 (root)   |  ----->  | uid 100000 (無権限) |
  | uid 1 (daemon) |  ----->  | uid 100001          |
  | ...            |          | ...                 |
  | uid 65535      |  ----->  | uid 165535          |
  +----------------+          +---------------------+

  /etc/subuid、/etc/subgid がマッピング可能な範囲を定義
  newuidmap/newgidmap ヘルパーがマッピングを記録
# ホストでマッピング範囲を確認
cat /etc/subuid    # 例: youngju:100000:65536
cat /etc/subgid

# 実行中のプロセスのマッピング確認 (実際のPIDに置き換えて)
cat /proc/12345/uid_map

このマッピングのおかげで、Podmanや最近のDocker rootlessモードはデーモンさえ一般ユーザー権限で動かします。コンテナエスケープに成功しても、攻撃者が得るのはホストの無権限UIDなので、被害範囲が大きく縮小します。Kubernetesもuser namespaceサポート(hostUsers false)が1.33でベータ既定有効化を経て安定化の段階に入りました。

cgroup v2 — 統合階層とファイルインターフェース

ネームスペースが「視界」なら、cgroupは「上限」です。cgroup v2はv1のコントローラ別分離階層を単一ツリーに統合し、現在は主要ディストリビューションとKubernetesの既定です。

   /sys/fs/cgroup            (ルート、cgroup2マウント)
   |-- cgroup.controllers     使用可能なコントローラの一覧
   |-- cgroup.subtree_control 子に委譲するコントローラ
   |-- system.slice/          systemdサービス
   |-- user.slice/            ログインセッション
   +-- kubepods.slice/        KubernetesのPod
       +-- kubepods-burstable.slice/
           +-- kubepods-burstable-pod<hash>.slice/
               +-- cri-containerd-<hash>.scope/
                   |-- cpu.max
                   |-- memory.max
                   |-- io.max
                   +-- pids.max

すべての制御がファイルの読み書きであるところがcgroupの美しさです。直接操作してみます。

# 新しいcgroupの作成とコントローラの有効化
sudo mkdir /sys/fs/cgroup/lab
echo "+cpu +memory +io +pids" | sudo tee /sys/fs/cgroup/cgroup.subtree_control

# cpu: 100ms周期あたり20ms = 0.2 CPU
echo "20000 100000" | sudo tee /sys/fs/cgroup/lab/cpu.max

# memory: ソフト圧迫ライン192MiB、ハード上限256MiB
echo "201326592" | sudo tee /sys/fs/cgroup/lab/memory.high
echo "268435456" | sudo tee /sys/fs/cgroup/lab/memory.max

# pids: フォークボム防止
echo 128 | sudo tee /sys/fs/cgroup/lab/pids.max

# 現在のシェルを入れて確認
echo $$ | sudo tee /sys/fs/cgroup/lab/cgroup.procs
cat /sys/fs/cgroup/lab/memory.current   # 現在の使用量
cat /sys/fs/cgroup/lab/cpu.stat         # usage、nr_throttled
cat /sys/fs/cgroup/lab/memory.events    # low/high/max/oom/oom_kill回数

memory.high vs memory.max — OOMの二つの顔

この2つの違いは運用では決定的です。

項目memory.highmemory.max
超過時の動作回収圧力 + 割り当て速度の強制低下回収を試み、失敗すればOOM Kill
プロセスの生存死なない (遅くなる)oom_killで即死しうる
用途漸進的な圧力、早期警報最後の防衛線
観測memory.eventsのhighカウントmemory.eventsのoom_killカウント

memory.highだけを超過したプロセスは死なない代わりに、メモリ割り当てが遅くなり回収(reclaim)に時間を奪われます。「Podは死なないのに突然遅くなった」なら、memory.eventsのhighカウントとPSI(pressure stall information)を確認してください。

# メモリ圧迫の正直な指標: PSI
cat /sys/fs/cgroup/lab/memory.pressure
# some avg10=12.34 ... 一部のタスクがメモリ待ちに時間を使った
# full avg10=3.21  ... すべてのタスクが同時に停止 (深刻)

Kubernetesのmemory limitsはmemory.maxで実装されています。limits超過時にコンテナがOOMKilledで再起動される理由がまさにこれです。

ミニコンテナを自作する

学んだピースを結合してコンテナを自分で作ります。まずbash版です。

#!/bin/bash
# mini-container.sh — unshare + pivot_root + cgroup の結合
# 事前準備: ルートFS (例: alpineミニルートFSを展開したディレクトリ)
set -euo pipefail

ROOTFS=/opt/rootfs-alpine
CG=/sys/fs/cgroup/minic

# 1) cgroupの準備: 0.5 CPU、256MiB、プロセス64個
mkdir -p "$CG"
echo "50000 100000" > "$CG/cpu.max"
echo "268435456"    > "$CG/memory.max"
echo 64             > "$CG/pids.max"

# 2) ネームスペースを作り、その中でセットアップスクリプトを実行
exec unshare --pid --fork --mount --uts --ipc --net bash -c '
  set -euo pipefail
  ROOTFS=/opt/rootfs-alpine

  # 2-1) 自分をcgroupに登録
  echo $$ > /sys/fs/cgroup/minic/cgroup.procs

  # 2-2) ホスト名
  hostname minic

  # 2-3) マウント伝播の遮断 (ホスト汚染の防止)
  mount --make-rprivate /

  # 2-4) pivot_rootの準備: new_rootはマウントポイントである必要がある
  mount --bind "$ROOTFS" "$ROOTFS"
  cd "$ROOTFS"
  mkdir -p old_root

  # 2-5) ルートの交換。chrootと違い脱出が難しい
  pivot_root . old_root

  # 2-6) 新ルート基準の必須仮想ファイルシステム
  mount -t proc  proc  /proc
  mount -t tmpfs tmpfs /tmp

  # 2-7) 旧ルートへのアクセス遮断
  umount -l /old_root
  rmdir /old_root

  # 2-8) コンテナのinitを実行
  exec /bin/sh
'

重要なポイントは3つです。第一に、chrootではなくpivot_rootを使います。chrootは視界を変えるだけですが、pivot_rootはマウントネームスペースのルート自体を交換し旧ルートを切り離すため、脱出がはるかに困難です。第二に、mount --make-rprivateでマウントイベントの伝播を断たないと、コンテナ内のumountがホストへ伝播しうるのです。第三に、procはpidネームスペースの中で再マウントしないと、psが隔離された視界を見せてくれません。

同じものをGoで作るとruncの縮小版になります。

// minic.go — Goで作るミニコンテナ (概念検証用)
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"syscall"
)

func main() {
	switch os.Args[1] {
	case "run": // 親: ネームスペースを持って自分自身を再実行
		parent()
	case "child": // 子: 新しいネームスペース内でセットアップ
		child()
	default:
		panic("usage: minic run <cmd>")
	}
}

func parent() {
	cmd := exec.Command("/proc/self/exe",
		append([]string{"child"}, os.Args[2:]...)...)
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
			syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC |
			syscall.CLONE_NEWNET,
	}
	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
	if err := cmd.Run(); err != nil {
		fmt.Println("error:", err)
		os.Exit(1)
	}
}

func child() {
	rootfs := "/opt/rootfs-alpine"

	// cgroup登録 (v2ファイルインターフェースそのまま)
	cg := "/sys/fs/cgroup/minic"
	os.MkdirAll(cg, 0755)
	os.WriteFile(filepath.Join(cg, "memory.max"),
		[]byte("268435456"), 0644)
	os.WriteFile(filepath.Join(cg, "pids.max"), []byte("64"), 0644)
	os.WriteFile(filepath.Join(cg, "cgroup.procs"),
		[]byte(fmt.Sprint(os.Getpid())), 0644)

	syscall.Sethostname([]byte("minic"))

	// マウント伝播の遮断 + pivot_root
	syscall.Mount("", "/", "", syscall.MS_REC|syscall.MS_PRIVATE, "")
	syscall.Mount(rootfs, rootfs, "", syscall.MS_BIND, "")
	os.MkdirAll(rootfs+"/old_root", 0700)
	syscall.PivotRoot(rootfs, rootfs+"/old_root")
	os.Chdir("/")
	syscall.Mount("proc", "/proc", "proc", 0, "")
	syscall.Unmount("/old_root", syscall.MNT_DETACH)
	os.Remove("/old_root")

	// ユーザーが与えたコマンドを実行
	cmd := exec.Command(os.Args[2], os.Args[3:]...)
	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
	cmd.Run()
}
go build -o minic minic.go
sudo ./minic run /bin/sh
# 中で: hostname -> minic、ps aux -> PID 1から、ip link -> loのみ

70行程度でDockerの核心的な隔離が再現できます。プロダクションランタイムとの違いはこの先です — イメージ管理、ネットワーク配線、そしてセキュリティ層。

overlayfs — イメージレイヤーの原理

コンテナイメージのレイヤー構造はoverlayfsで実装されています。

   コンテナが見るファイルシステム (merged)
  +------------------------------------+
  |  /bin/sh   /etc/app.conf   /tmp/x  |
  +------------------------------------+
      ^ 合成された単一ビュー
      |
  upperdir (書き込み可能、コンテナレイヤー)   <- 変更分だけ記録
  +------------------------------------+
  |  /etc/app.conf (修正版)  /tmp/x    |
  +------------------------------------+
  lowerdir (読み取り専用、イメージレイヤー群)  <- 複数重ねられる
  +------------------------------------+
  |  layer3: アプリのバイナリ           |
  |  layer2: ランタイム/ライブラリ      |
  |  layer1: ベースOS                  |
  +------------------------------------+

  - 読み取り: 上から探索、最上位のファイルが勝つ
  - 書き込み: lowerdirのファイル修正時はupperdirへコピーしてから修正
              (copy-up)
  - 削除:     upperdirにwhiteoutマーカーを作成 (実際には消えない)
# overlayfsの直接実習
mkdir -p /tmp/ov/lower /tmp/ov/upper /tmp/ov/work /tmp/ov/merged
echo "from-image" > /tmp/ov/lower/base.txt
sudo mount -t overlay overlay \
  -o lowerdir=/tmp/ov/lower,upperdir=/tmp/ov/upper,workdir=/tmp/ov/work \
  /tmp/ov/merged

echo "modified" > /tmp/ov/merged/base.txt   # copy-up発生
cat /tmp/ov/lower/base.txt                  # from-image (原本保存)
cat /tmp/ov/upper/base.txt                  # modified (変更分)

同じイメージを使う100個のコンテナがlowerdirを共有し、それぞれ薄いupperdirだけを持つため、コンテナの作成が速くディスクが節約されます。その代償としてcopy-upコストがあるため、大きなファイルをコンテナレイヤーで修正するワークロード(例: DBデータ)は必ずボリュームを使うべきです。

capabilities — rootを分割する

伝統的なUnixの権限はroot(全能)か一般ユーザー(無力)かの二分法でした。capabilitiesはrootの権限を約40個の断片に分割し、必要なものだけを付与します。

# 現在のプロセスのケーパビリティ確認
capsh --print
grep Cap /proc/self/status   # CapEffが有効ケーパビリティのビットマスク
capsh --decode=00000000a80425fb   # ビットマスクの解読

# ファイルへのケーパビリティ付与 (setuid rootの代替)
sudo setcap cap_net_bind_service=+ep ./myserver  # 80番ポートのバインドだけ許可

コンテナランタイムは既定でケーパビリティを大幅に削って起動します。Docker/containerdの既定セットはCHOWN、DAC_OVERRIDE、FOWNER、SETUID、SETGID、NET_BIND_SERVICE、KILLなど十数個で、SYS_ADMIN(事実上root級)、NET_ADMIN、SYS_PTRACEのような危険なものは除外されます。

# Kubernetes: すべて捨てて必要なものだけ追加するのが模範
securityContext:
  capabilities:
    drop: ["ALL"]
    add: ["NET_BIND_SERVICE"]
  allowPrivilegeEscalation: false

privileged trueは、これらすべての保護(ケーパビリティ制限、seccomp、デバイス隔離)を一度に解除するスイッチです。「うまく動かないからprivileged」は、コンテナのセキュリティモデル全体を捨てる決定だと認識すべきです。

seccomp — システムコールのフィルタ

seccompは、プロセスが呼び出せるシステムコール自体をBPFフィルタで制限します。Linuxのシステムコールは400を超えますが、一般的なアプリケーションが使うのは一部です。既定プロファイルはkeyctl、add_key、kexec_load、open_by_handle_at(コンテナエスケープの実例で使われたコール)などの危険なコールを遮断します。

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": ["read", "write", "openat", "close", "fstat",
                 "mmap", "brk", "exit_group", "futex", "epoll_wait"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}
# プロセスのseccomp状態: 0 なし、1 strict、2 filter
grep Seccomp /proc/self/status

# Kubernetesでランタイム既定プロファイルを適用
# securityContext.seccompProfile.type: RuntimeDefault

実務の基準: RuntimeDefaultを全ワークロードの既定として敷き、遮断によるエラー(EPERM)が疑われたらstraceで確認し、カスタムプロファイルで狭く許可する順序が安全です。

ランタイムは何をするのか — runcの仕事

これでランタイム層の分業が明確になります。

  kubelet
    | CRI (gRPC)
    v
  containerd          イメージのpull/保存、スナップショット(overlayfs)、
    |                  コンテナのライフサイクル
    | OCIスペック (config.json)
    v
  runc                この記事で扱ったすべての実行者:
                      ネームスペース作成、cgroup設定、pivot_root、
                      capabilities適用、seccompロード、プロセスexec
                      -- 実行後は消える (デーモンではない)

runcはOCIランタイムスペックのconfig.json(ネームスペースの一覧、cgroup上限、ケーパビリティ、seccomp、マウント、rootfsパス)を受け取り、私たちが上で手作業でやったことを正確かつ安全に遂行するツールです。私たちのミニコンテナと本質は同じです。

# 実行中のコンテナのOCI設定を覗く
sudo cat /run/containerd/io.containerd.runtime.v2.task/k8s.io/*/config.json \
  | jq '.linux.namespaces, .process.capabilities.effective' | head -30

Kubernetesとのつながり — QoSクラスとcgroupドライバ

Kubernetesの抽象化がcgroupツリーにどう着地するかを整理します。

QoSクラス条件cgroup上の意味
Guaranteed全コンテナで requests = limitscpu.max固定、OOMスコア最低(保護最大)
Burstablerequestsあり、limitsと異なるcpu.weight比例配分、中間の保護
BestEffortrequests/limitsなし最小weight、OOMの第一犠牲者

メモリ圧迫時、カーネルのOOM Killerはoom_score_adjが高い順に殺しますが、kubeletがQoSに応じてこの値を設定します。BestEffortが先に死ぬのはどこかのポリシーではなく、cgroupとOOMスコアの直接の帰結です。

cgroupドライバはcgroupfsではなくsystemdドライバが標準です。cgroupツリーの管理主体が2つ(systemdとkubelet)になると衝突するからです。cgroup v2前提の機能 — memory.highベースのメモリQoS(MemoryQoSフィーチャーゲート)、swap制御、PSIベースの退避シグナル — が増え続けているため、ノードがv2かの確認は基本です。

stat -fc %T /sys/fs/cgroup    # cgroup2fs ならv2

デバッグ道具箱

抽象化を剥がして直接見る道具たちです。

# 1) lsns: ホストの全ネームスペースを列挙
lsns                       # タイプ別ネームスペースと代表PID
lsns -t net                # netネームスペースのみ

# 2) nsenter: 特定プロセスのネームスペースへ進入
PID=$(pgrep -f my-app | head -1)
nsenter -t "$PID" -n ss -tlnp      # そのコンテナのnet nsでソケットを見る
nsenter -t "$PID" -m -p ps aux     # mnt+pid nsでプロセスを見る
nsenter -t "$PID" -a bash          # すべて進入 (事実上のexec)
# デバッグツールのないdistrolessコンテナもnsenterで調査可能
# (バイナリはホストのものを使い、視界だけコンテナへ)

# 3) /procでネームスペースIDを直接比較
ls -l /proc/$$/ns/         # 各nsのinode番号
ls -l /proc/"$PID"/ns/     # 同じinode = 同じネームスペース

# 4) cgroupパスの追跡: プロセス -> cgroup -> 上限/使用量
cat /proc/"$PID"/cgroup    # cgroupパスの確認
CG=/sys/fs/cgroup$(cut -d: -f3 /proc/"$PID"/cgroup)
cat "$CG/memory.current" "$CG/memory.max" "$CG/memory.events"
cat "$CG/cpu.stat" | grep -E "usage|throttled"
cat "$CG/memory.pressure" "$CG/cpu.pressure"   # PSI

# 5) OOM Killの事後分析
dmesg -T | grep -i -A5 "killed process"
journalctl -k | grep -i oom

特に「PodがOOMKilledなのにグラフ上のメモリはlimits以下だった」という事件は、グラフの解像度(通常15秒以上)と瞬間スパイクの差か、ページキャッシュを含むmemory.currentとRSSだけを見るグラフの差であるケースがほとんどです。memory.eventsのoom_killカウンタとdmesgが真実を語ってくれます。

落とし穴とアンチパターン

  • privileged trueをデバッグの利便性で乱用すること。隔離モデル全体が解除されます。必要なケーパビリティだけを追加するのが正解です。
  • コンテナをVMのように考え、カーネル共有を忘れること。1つのコンテナのカーネルパニック/エクスプロイトはノード全体の問題です。
  • DBデータをコンテナレイヤー(overlayfs upperdir)に書くこと。copy-upコストと揮発性の両面で誤りです。
  • memory limitsなしのBestEffortで重要なワークロードを動かすこと。圧迫時の第一犠牲者です。
  • PID 1問題を忘れること。コンテナの最初のプロセスはシグナルの既定動作が異なり、ゾンビ回収の責任があります。tiniのような軽量initやシェルのexecパターンを使いましょう。
  • cgroup v1時代の知識(memory.limit_in_bytesなどのファイル名)でv2ノードを操作しようとすること。インターフェースが異なります。
  • nsenterを使わずコンテナ内にデバッグツールをインストールしようとすること。イメージの不変性を壊し再現性を損ないます。

運用チェックリスト

  • ノードがcgroup v2か、cgroupドライバがsystemdかを確認したか
  • 全ワークロードにseccomp RuntimeDefaultが既定適用されているか
  • capabilitiesはdrop ALL後に必要なものだけ追加しているか
  • privilegedコンテナの存在理由が文書化され定期レビューされているか
  • rootless/userネームスペース適用の可能性を評価したか
  • memory.events(oom_kill、high)とPSIをノード/Pod指標として収集しているか
  • QoSクラス設計(何がGuaranteedであるべきか)が意図的か
  • 書き込みの多いデータ経路がoverlayfsではなくボリュームを使っているか
  • PID 1のシグナル/ゾンビ処理が全イメージで解決されているか
  • lsns/nsenterベースのデバッグ手順がランブックにあるか

おわりに

「コンテナは嘘」というタイトルは非難ではなく賛辞です。ネームスペース、cgroups、capabilities、seccomp、overlayfs — それぞれ独立に進化したカーネル機能が組み合わさり、まるで隔離された小さなマシンが存在するかのような幻想を作り出します。良い抽象化がいつもそうであるように、普段は幻想を楽しめばよいのです。しかし障害とセキュリティの瞬間には、幻想を見抜いて/sys/fs/cgroupと/proc配下のnsリンクを直接読める人が問題を解決します。この記事の実習がその透視力の出発点になることを願います。

参考資料