Skip to content

✍️ 필사 모드: コンテナとDocker内部完全攻略 — Namespace、cgroups、OverlayFS、seccomp、Capabilities、そしてKubernetesまで (2025)

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

0. 「コンテナはVMではない」

多くのエンジニアはコンテナを「軽量VM」と捉えているが、実際にはまったく違う:

項目VMContainer
隔離単位ハードウェアOS Namespace
カーネルVMごとに独立ホストと共有
起動時間秒〜分数十ms
メモリオーバヘッド数百MB数MB
ディスクオーバヘッドGBMB
隔離強度強い弱い

コンテナ = プロセス + カーネル機能 (Namespace, cgroup, rootfs) の組み合わせ。別のOSがある訳ではない。

本記事は docker run nginx という一行がLinuxカーネル内で実際に何をするか、その裏にある10年以上のエンジニアリング史を掘り下げる。

1. コンテナの短くない歴史

1.1 1979 — chroot

Unix v7の chroot() システムコール。「このプロセスのファイルシステムルートを変える」→ 限定されたディレクトリしか見えない。ただしプロセス一覧、ネットワーク、ユーザはそのまま見え、chdirやmountで脱出できた。

1.2 2000 — FreeBSD Jails

chrootを拡張し、ネットワーク・プロセス・ユーザまで隔離。本当の「軽量仮想化」の始まり。

1.3 2004 — Solaris Zones

完全な隔離技術。コンテナ概念の商業的初成功例。

1.4 2006 — cgroups、2007 — LXC

Googleエンジニアが内部リソース隔離のため「process containers」を開発 → Linuxのcgroupsへ。Namespaceも段階的に追加。LXCがこれらを組み合わせた最初の実用的コンテナランタイムを提供。

1.5 2013 — Docker

Solomon HykesのdotCloudがLXCを使いやすいAPIで包装。docker run 一行の魔法。キラー機能は イメージレイヤ (OverlayFS)レジストリ (Docker Hub)

1.6 2015 — OCI、2016 — containerd、2017 — Kubernetes

OCI (Open Container Initiative) がイメージ/ランタイムの標準を確立。Dockerはランタイムをcontainerdへ分離。Kubernetesがオーケストレーション標準となり、Docker Engineは抽象化されていった。

1.7 2024 — 現在

Docker Desktop、Podman (daemonless)、Kubernetes + containerd + runc、ECS、Cloud Run、Fly.io。コンテナはすべてのクラウドデプロイの基本単位となった。

2. Linux Namespace — 隔離の7次元

Namespaceは「このプロセスが見えるOSリソース」を制限する。2024年時点で7種類。

2.1 PID Namespace

プロセスID隔離。コンテナ内で ps すると:

PID TTY      TIME CMD
  1 ?     00:00:00 nginx
 10 ?     00:00:00 worker
 11 ?     00:00:00 worker

コンテナ内のnginxがPID 1、ホストではPID 12345。別の番号空間だ。PID 1は特別で、カーネルはゾンビリープ (init役割) を期待し、終了すると全子プロセスも終了する。多くのアプリはinitとして設計されていないため tinidumb-init のような小さなinitが使われる。

2.2 Mount Namespace

マウントポイントの隔離。各コンテナが独自の / ツリーを持つ。chrootの進化系。ホストの特定マウントを共有可能。

2.3 Network Namespace

各コンテナが独自のネットワークインターフェース、ルーティングテーブル、iptablesルールを持つ。

# ホストからコンテナのNetwork Namespaceへ入る
sudo nsenter -t <container_pid> -n ip addr

Dockerのbridgeモード: 仮想ブリッジ docker0 + vethペア。

2.4 UTS Namespace

Unix Timesharing System。ホスト名・ドメインを隔離。

2.5 IPC Namespace

System V IPC、POSIXメッセージキューを隔離。

2.6 User Namespace

UID/GIDマッピング。コンテナ内のroot (UID 0) がホストでは非特権ユーザ UID 100000 にマップされる。セキュリティ上決定的だが設定が複雑で、Dockerはデフォルト無効。Podman rootlessが活用。

2.7 Cgroup Namespace

cgroup階層を隔離。コンテナは自分のcgroupパスを / として見る。2016年導入。

2.8 Time Namespace

Linux 5.6 (2020) で追加。各コンテナが独自の CLOCK_BOOTTIME を持てる。チェックポイント/復元 (CRIU) で利用。

2.9 Namespace操作

unshare --pid --mount --net --fork bash   # 新しいNamespaceで bash
nsenter -t PID -n -p                       # 既存Namespaceへ侵入

docker exec は実質、対象コンテナのNamespaceに入って新しいプロセスを生成する。

3. cgroups — リソース制限

3.1 cgroups v1 (2007-)

リソースごと (CPU、memory、networkなど) に別階層:

/sys/fs/cgroup/
├── cpu/
│   ├── docker/
│   │   └── <container_id>/
│   │       ├── cpu.cfs_quota_us
│   │       └── cpu.cfs_period_us
├── memory/
├── blkio/
└── ...

独立した階層で複雑。

3.2 cgroups v2 (2016-、systemd 232+)

単一の統合階層:

/sys/fs/cgroup/
├── user.slice/
├── system.slice/
│   └── docker-<id>.scope/
│       ├── cpu.max         # "100000 100000" = 1 CPU
│       ├── memory.max      # "536870912" = 512MB
│       ├── io.max
│       └── pids.max

一つのツリー、階層的リソース配分、よりシンプルなモデル。

3.3 CPU制限の実際

cpu.max = "50000 100000" は「100ms周期ごとに最大50ms CPU使用」= 0.5 CPU。JVMやNodeが短時間CPUを爆食いするとthrottleがかかり応答遅延。Dockerの --cpus=1cpu.max = "100000 100000"

3.4 メモリ制限

  • memory.max: 上限。超えるとOOM。
  • memory.high: ソフト制限。越えるとreclaim圧力。
  • memory.minmemory.low: 保護メモリ。

コンテナが memory.max に達すると コンテナ内OOM Killer が発動。ホストは無事。

3.5 JVM、Node、Goのcgroup認識

JVM 10+ は -XX:+UseContainerSupport がデフォルトでcgroup上限を読む。Go 1.19+ は GOMEMLIMIT。Nodeは --max-old-space-size。これを設定しないとホストメモリ64GBと誤認してヒープを大きく取り、OOMする。

4. OverlayFS — イメージレイヤの秘密

4.1 なぜレイヤか

Dockerイメージは 読み取り専用レイヤのスタック:

Layer 4 (10KB): アプリコード
Layer 3 (50MB): npm install 結果
Layer 2 (80MB): apt packages
Layer 1 (70MB): ubuntu base

各レイヤは独立・不変。複数イメージがbaseレイヤを共有。100GBイメージでも変更されたtop layerだけpush/pull。

4.2 OverlayFSの構造

┌─────────────────────┐
upperdir (RW)      │  ← コンテナ書き込み
├─────────────────────┤
│  merged view        │  ← アプリが見るFS
├─────────────────────┤
lowerdir (RO) ×N   │  ← イメージレイヤ
└─────────────────────┘

読み込み: upperdirに無ければlowerdir。書き込み: 常にupperdir。修正は Copy-up (元をupperdirへコピーしてから修正)。

4.3 Copy-upのコスト

100MBファイルに1バイト変更 = 100MBコピー。大きなDBファイルをコンテナ内に置かない理由。解決: Volume mount

4.4 Whiteout — ファイル削除

rm してもlowerdirからは消えない。代わりにupperdirに特殊な「whiteout」ファイルを作成しmerged viewから隠す。実ファイルは下に残る。Dockerfileで後段の RUN rm をしてもレイヤサイズは減らない。同じ RUN 内で生成+削除する必要がある。

4.5 Storage driverの進化

  • aufs (2013-): 初期、upstreamなし。
  • devicemapper (2014-): thin provisioning、遅い。
  • btrfs、zfs: ファイルシステム依存。
  • overlay (2014): upstream、速い。
  • overlay2 (2016-): 現在のデフォルト。複数lowerdir対応。

Podmanはrootless対応のfuse-overlayfsも選択可能。

5. コンテナランタイム — runcと仲間たち

5.1 Dockerの内部階層

docker CLI
dockerd (daemon)
containerd (コンテナライフサイクル)
containerd-shim (コンテナごとに1)
runc (OCI runtime、実際のコンテナ生成)
Linux kernel (Namespace、cgroup、...)

5.2 runcの役割

OCI Runtime Specificationを実装。入力: rootfs + config.json。出力: 動いているコンテナ。Go/Cで書かれ、clone() + Namespaceフラグ、setns()、cgroup設定、rootfs mount、execve() を直接呼ぶ。

5.3 代替ランタイム

  • crun: C実装、runcより速い (Goランタイムなし)。
  • youki: Rust実装。
  • Kata Containers: 各コンテナを軽量VM内で実行 → VM級の隔離。
  • gVisor: Google。ユーザ空間カーネルでシステムコールを捕獲。
  • Firecracker: AWS LambdaのmicroVM。

マルチテナント環境 (Lambda、Fargate) はVMベースランタイムを採用。

5.4 shimの役割

containerd-shim はコンテナごとに存在。containerd daemonが死んでもコンテナは生き残る。stdout/stderr収集、exit code記録。kubelet → CRI → containerd → shim → runc。

6. セキュリティ — コンテナが「本物の隔離」ではない理由

6.1 共有カーネルのリスク

コンテナは同じカーネルを共有 → カーネル脆弱性 = 全コンテナ脱出。実例:

  • Dirty COW (CVE-2016-5195): 権限昇格、コンテナ脱出。
  • runc CVE-2019-5736: runcバイナリ上書きでホストroot奪取。
  • Leaky Vessels (2024): runc、containerdの複数脆弱性。

対策: カーネル更新、AppArmor/SELinux、seccompプロファイル。

6.2 Linux Capabilities

従来のUnix: root (UID 0) = 全権限。コンテナには危険すぎる。Capabilitiesはrootを細分化:

  • CAP_NET_ADMIN: ネットワーク設定。
  • CAP_SYS_ADMIN: 大部分の危険な操作。
  • CAP_NET_BIND_SERVICE: 1024以下のポートバインド。

Dockerデフォルト: 制限されたcap set (最小権限)。--cap-add--cap-drop で調整。--privileged は全cap = ホストrootとほぼ同等。

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

「このコンテナは特定syscallのみ許可」:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    { "names": ["read", "write", "open"], "action": "SCMP_ACT_ALLOW" }
  ]
}

Dockerのデフォルトseccompプロファイルは約300のsyscallを許可し、keyctlptracemount などを遮断。

6.4 AppArmor / SELinux

ファイルシステムレベルのMAC (Mandatory Access Control)。UbuntuはAppArmor、RHELはSELinux。

6.5 Rootless Container

コンテナdaemonも非特権ユーザで実行。User NamespaceでUIDマッピング。Podmanのデフォルト。Dockerもrootlessモード対応。セキュリティ大幅強化、一部機能制限 (1024以下のポートなど)。

6.6 セキュリティチェックリスト

  • 最新カーネル。
  • デフォルトseccompプロファイル維持 (--security-opt seccomp=unconfined 禁止)。
  • Capability最小化 (--cap-drop ALL + 必要なもののみadd)。
  • Non-rootユーザでアプリ実行 (USER ディレクティブ)。
  • Read-only rootfs (--read-only + tmpfs volumes)。
  • Image scanning: Trivy、Grype、Docker Scout。
  • Runtime monitoring: Falco。

7. イメージ作成の技術

7.1 レイヤ最適化

# 悪い: 各RUNが新レイヤ
FROM node:20
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
RUN rm -rf /tmp/cache
# 良い: 同じRUN内でクリーンアップ
FROM node:20
COPY package.json .
RUN npm install && rm -rf /tmp/* /var/cache/*
COPY . .
RUN npm run build

7.2 Multi-stage Build

FROM node:20 AS builder
WORKDIR /app
COPY . .
RUN npm install && npm run build

FROM node:20-alpine
COPY --from=builder /app/dist /app
COPY --from=builder /app/node_modules /app/node_modules
CMD ["node", "/app/index.js"]

ビルドツール (gcc、webpackなど) は最終イメージに入らない → サイズ5倍以上削減。

7.3 Distroless Image

Googleが作る最小イメージ (shellすら無し):

FROM gcr.io/distroless/nodejs20
COPY --from=builder /app /app
CMD ["/app/index.js"]

サイズ数十MB、攻撃表面極小。短所はデバッグが難しい (execで入れない)。

7.4 Alpine vs Debian

  • Alpine (node:20-alpine): 5MB base、musl libc。
  • Debian slim (node:20-slim): 80MB、glibc。

Alpineの注意: musl互換性問題 (DNS resolver、pthread挙動の微差)。PythonのC extensionsビルドには追加ツール必要。

7.5 BuildKitとキャッシュ

DOCKER_BUILDKIT=1 docker build .

並列stageビルド、キャッシュのexport/import (--cache-to=type=registry,ref=...)、ビルドタイムsecrets (--secret)、containerdネイティブ。

8. ネットワーキング — docker0からCNIまで

8.1 Docker デフォルト bridge

docker0 (bridge, 172.17.0.1)
  ├── veth0 → container1 eth0 (172.17.0.2)
  ├── veth1 → container2 eth0 (172.17.0.3)
  └── veth2 → container3 eth0 (172.17.0.4)

vethペア (片方ホスト、片方コンテナ)、iptables MASQUERADE でNAT、-p 80:8080 はiptables DNAT。

8.2 ネットワークモード

  • bridge (デフォルト)。
  • host: ホストネットワーク共有 (隔離無し、最速)。
  • none: ネットワーク無し。
  • container: 他コンテナと共有 (podに似る)。

8.3 Kubernetes CNI

Kubernetesは「全podが平らなIPを持つ」哲学。Docker bridgeでは不十分。

  • Flannel: VXLANトンネリング、シンプル。
  • Calico: BGPベース、eBPF対応、ネットワークポリシー。
  • Cilium: eBPFネイティブ、観測性/セキュリティ豊富。
  • AWS VPC CNI: ENIをpodに直接割当。

CNI = Container Network Interface。プラグイン交換可能な標準。

9. Kubernetesとの統合

9.1 なぜKubernetesはDockerを捨てたか (2020)

Kubernetes 1.20でdockershim非推奨を告知、1.24 (2022) で削除。理由: Docker Engineはkubeletが使わない機能 (イメージビルド、swarmなど) を多数含む。CRIに合わせるためのshim層が余計。containerdが既にDockerの中核。結果: kubelet → CRI → containerd直結。OCIイメージはそのまま動作。

9.2 Pod = Namespaceを共有するコンテナ群

PodはNetwork、IPC、UTS NamespaceをコンテナたちでShareする。Mount、PIDは(デフォルト)独立。

pause container (Namespace所有者)
  ├── app container
  └── sidecar container

pause は何もしないミニプロセス。Namespaceだけ維持。

9.3 Init container と sidecar

  • Init container: Pod起動前に順次実行。
  • Sidecar (native 2023+): アプリと並行実行、独立リスタート可能。

Envoy (Istio)、Fluent Bitなどの補助プロセスがsidecarで。

9.4 オーケストレーションの意味

スケジューリング、ヘルスチェックと自動再起動、ローリング更新/canary、サービス検出/ロードバランシング、ストレージ/Config/Secret、水平オートスケーリング — すべて宣言的YAMLで。

10. 実践Tips

10.1 デバッグツールキット

docker ps -a
docker logs -f <container>
docker exec -it <container> sh
docker inspect <container>
docker stats
docker events

Kubernetes:

kubectl logs -f <pod>
kubectl exec -it <pod> -- sh
kubectl describe pod <pod>
kubectl get events --sort-by='.lastTimestamp'

10.2 Distrolessのデバッグ

shellが無いため exec で入れない。代替:

kubectl debug <pod> --image=busybox --target=app
# または
docker run --rm -it --pid=container:<id> --net=container:<id> busybox

pid/net Namespaceを共有してデバッグコンテナでアクセス。

10.3 イメージサイズ分析

  • docker history <image>: レイヤ別サイズ。
  • dive tool: レイヤ内容探索。
  • docker image ls --format="..."

10.4 性能観測

docker stats --no-stream

cat /sys/fs/cgroup/memory.current
cat /sys/fs/cgroup/cpu.stat

docker run --cap-add SYS_ADMIN --pid host ...

11. おわりに — docker run 一行の深さ

docker run nginx 一行が実際にやること:

  1. Docker Hubからnginxイメージレイヤをダウンロード。
  2. OverlayFSでレイヤスタック組立。
  3. containerdがcontainerd-shimを生成。
  4. shimがruncを起動。
  5. runcが7つのNamespace + cgroup + rootfsを設定。
  6. seccomp/AppArmorプロファイル適用。
  7. execve("nginx") でプロセス起動。
  8. コンテナネットワーク (veth + bridge) 構成。
  9. iptablesポートフォワーディング追加。

50ms以内に完了する。2000年代にVMを起動するのに数分かかっていたのと比べれば奇跡だ。背後には2006 cgroups、2008 Namespace、2013 Docker、2015 OCI、2016 overlay2、2020 CRIなど10年超のエンジニアリングがある。

同時にこの利便性の代価は カーネル共有 = セキュリティ弱点。Lambda、Fargateのような商用プラットフォームがmicroVMを使う理由。マルチテナントなら「コンテナ = 隔離」を全面的に信用するな。

次回は Kubernetes内部 — etcdのRaft合意、スケジューラのフィルタ/スコアリング、コントローラパターン、カスタムリソース、CRI/CNI/CSIのプラグインシステム — を掘り下げる。

参考資料

  • Jérôme Petazzoni — "Anatomy of a Container" (LinuxCon 2015)。
  • Michael Kerrisk — "Understanding Linux Namespaces" (LWN シリーズ)。
  • OCI Image/Runtime Specifications (GitHub)。
  • Julia Evans — Container Networking シリーズ。
  • Liz Rice — "Container Security" (O'Reilly, 2020)。
  • Kubernetes公式ドキュメント — Pods、CNI、CRI。
  • Red Hat crunブログ。
  • Firecracker論文 (NSDI 2020)。
  • Google gVisor論文。
  • "The Kubernetes Book" — Nigel Poulton。

현재 단락 (1/232)

多くのエンジニアはコンテナを「軽量VM」と捉えているが、実際にはまったく違う:

작성 글자: 0원문 글자: 10,193작성 단락: 0/232