コンテナランタイムの内部(runc・OCI仕様)
コンテナ起動の中身を分解すると、runc が namespace・cgroup・capability・seccomp を仕様どおり組み立てているだけと分かる。OCI仕様と high/low-level の分業を原理から押さえます。
- 1.OCIは「イメージ仕様(layer群+config.jsonのtar)」と「ランタイム仕様(config.jsonで隔離・権限・cgroup・seccompを宣言した展開済みバンドル)」の2本立てで、両者を橋渡しするのがコンテナ起動の全体像。
- 2.runc(low-level)はconfig.jsonを読み、clone/unshareで名前空間を作り、cgroupへ登録し、pivot_rootでrootfsを切り替え、capability・seccomp・no_new_privsを設定してからexecveする。状態はOCI state JSONで表現される。
- 3.containerd等(high-level)はイメージ取得・展開・スナップショット・ネットワーク・ライフサイクル管理を担い、実際のプロセス生成はrunc互換ランタイムへ委譲する。両者の境界がOCIランタイム仕様。
コンテナ起動は「仕様に沿った組み立て作業」
「コンテナを起動する」という1コマンドの裏では、複数の独立した部品が決められた順序で組み上がっています。その設計図を標準化したのが OCI(Open Container Initiative) の仕様群です。コンテナは独自の実行形式ではなく、名前空間と cgroups というカーネル機能を、仕様どおりに組み合わせて作る 隔離されたプロセス にすぎません。
OCIには大きく2つの仕様があります。イメージ仕様は「持ち運ぶ配布物」を、ランタイム仕様は「展開して動かす実体」を規定します。両者を分けることで、ビルドツール・レジストリ・ランタイムが互いを知らなくても相互運用できます。
| 仕様 | 対象 | 中身の核 |
|---|---|---|
| イメージ仕様 | 配布形式(レジストリに置くもの) | layer群(tar+gzip)とimage config、それを束ねるmanifest |
| ランタイム仕様 | 実行形式(ディスク上に展開したもの) | rootfsディレクトリと、隔離・権限を宣言するconfig.json |
| 分配仕様(distribution) | レジストリとの送受信プロトコル | HTTP APIでのpush/pull、digestによる内容アドレス指定 |
イメージ仕様:レイヤと内容アドレス
OCIイメージは複数の レイヤ から成ります。各レイヤはファイルシステムの差分を収めたtarアーカイブで、削除は .wh.<名前> というwhiteoutファイルで表現されます。実行時にはこれらを下から順に重ね、overlayfs などで1枚のrootfsに合成します。レイヤを重ねる方式により、共通の下位レイヤを複数イメージで共有でき、ディスクと転送量を節約できます。
イメージの一貫性を支えるのが 内容アドレス(content-addressable) です。各要素は sha256:... のダイジェストで指され、manifestがlayer群とimage configのダイジェストを列挙し、configがエントリポイントや環境変数を保持します。中身が1バイトでも変わればダイジェストが変わるため、改竄や取り違えを検知できます。なお config.json という名前はイメージ仕様(image config)とランタイム仕様(runtime config)の両方に登場しますが、別物である点に注意してください。
ランタイム仕様:バンドルとconfig.json
high-level側がイメージを展開すると、rootfs/ ディレクトリと config.json を含む バンドル(OCI bundle) ができます。この config.json こそがランタイム仕様の中心で、「どう隔離し、どの権限を与え、何を実行するか」を 宣言的に 記述します。runcはこの宣言を読み、対応するカーネル操作へ翻訳するだけの存在です。
// config.json(抜粋・概念図)
{
"process": {
"args": ["/bin/sh"],
"capabilities": { "bounding": ["CAP_CHOWN", "CAP_NET_BIND_SERVICE"] },
"noNewPrivileges": true
},
"linux": {
"namespaces": [ { "type": "pid" }, { "type": "mount" }, { "type": "network" } ],
"resources": { "memory": { "limit": 536870912 } },
"seccomp": { "defaultAction": "SCMP_ACT_ERRNO" }
}
}
ランタイムの状態は OCI state(id・status・pid を持つJSON)として表現され、create → start → kill → delete というライフサイクル動詞で操作します。create で隔離環境を準備して停止状態にし、start で初めてユーザープロセスを走らせる二段構えになっているのが要点です。
runcの組み立て手順:順序が安全性を決める
runcが config.json を実体化する流れは、順序が安全性に直結 します。たとえば権限を落とす前にユーザープロセスを起動してはいけません。概略は次のとおりです。
- 名前空間の作成:
clone(2)/unshare(2)にCLONE_NEWPIDなどのフラグを渡し、必要な名前空間を作る。rootlessでは最初にuser名前空間を作って内側rootを得る。 - cgroupへの登録:cgroup v2 のサブツリーを作り、新プロセスのPIDを
cgroup.procsへ書き込んでCPU・メモリ上限を効かせる。 - rootfsの切り替え:マウントツリーをprivate化し、
pivot_root(2)で新rootへ差し替えてから古いrootをumountし、参照を断つ。 - マウント整備:
/proc・/sys・/devなどをconfig指定どおりにマウントし、必要箇所をread-only化する。 - 権限の確定:
no_new_privsを立て、bounding setからcapabilityを削り、最後にseccompフィルタを取り付ける。 - execve:上記をすべて適用した状態で、初めてユーザープロセスを
execve(2)で起動する。
seccompを設定するには通常 CAP_SYS_ADMIN が要りますが、no_new_privs を立てておけばcapabilityなしでもフィルタを取り付けられます。さらにこのフラグは 以後のexecveで特権を獲得する経路(setuidバイナリなど)を恒久的に封じる ため、権限縮小の前提として先に立てます。execveより前に no_new_privs → capability削減 → seccomp の順で確定させるのが定石です。
実装上の難所が、execveの直前に動く小さな補助プロセス init の存在です。clone直後の子プロセスはまだ隔離設定が済んでおらず、親から指示を受けて自分自身に名前空間参加・rootfs切替・権限縮小を適用してからexecveします。runcはこのブートストラップ部分をCで書き直し(nsexec)、Goランタイムがスレッドを起動する前に名前空間を確定させることで、マルチスレッドと setns(2) の相性問題を回避しています。
high-level と low-level の分業
docker run 一発の裏には階層化された分業があります。境界線にOCIランタイム仕様を置くことで、各層を差し替え可能にしているのが設計の妙です。
| 層 | 代表実装 | 責務 |
|---|---|---|
| CLI/デーモン | docker / kubelet(CRI) | ユーザー操作やオーケストレータからの指示を受ける |
| high-level runtime | containerd / CRI-O | イメージpull・展開、スナップショット、ネットワーク、ライフサイクル管理 |
| shim | containerd-shim | コンテナごとに常駐し、PID 1 の親としてプロセスを監視・収集 |
| low-level runtime | runc / crun / gVisor | config.jsonを実体化し、隔離環境を作って execve するだけ |
low-levelランタイムは 使い捨て である点が重要です。runcはコンテナを起動するとexecveで消え、常駐しません。コンテナの親プロセス(PID 1 の見守り役)を務めるのは shim で、これによりhigh-levelデーモンを再起動・更新してもコンテナが道連れに死なずに済みます。shimは終了コードの回収や標準入出力の中継も担います。
OCIランタイム仕様という共通の窓口があるため、low-level部分を目的に応じて差し替えられます。crun はCで書かれ起動が軽く、gVisor(runsc) はユーザー空間でシステムコールを代行してカーネル攻撃面を減らし、Kata Containers は軽量VMで動かしてハードウェア境界による隔離を得ます。high-level側はこれらを同じ create/start/delete で扱えます。
頻出は「OCIの2仕様の役割分担」と「high/low-levelの境界」。イメージ仕様=配布形式(layer+configのtar、内容アドレス)、ランタイム仕様=展開済みバンドル(rootfs+config.json)と create/start/delete のライフサイクル、と言い分けられること。runcの組み立ては「名前空間→cgroup→pivot_root→capability→seccomp→execve の順で、権限縮小はexecveの前」。「runcは使い捨て、PID 1 を見守るのはshim」「OCIランタイム仕様があるからcrun/gVisor等に差し替え可能」も押さえどころです。
まとめ
コンテナランタイムの正体は OCI仕様に沿った組み立て作業 です。イメージ仕様 がlayerと内容アドレスで配布形式を、ランタイム仕様 がrootfsと config.json で実行形式を規定し、両者の橋渡しがコンテナ起動です。runc はその config.json を読み、名前空間を作り、cgroupへ登録し、pivot_root でrootfsを切り替え、capability削減とseccompを no_new_privs の後に適用してからexecveする——順序こそが安全性です。そして containerd等のhigh-level がイメージ管理とライフサイクルを担い、実プロセス生成をrunc互換ランタイムへ委ね、shimがPID 1 を見守る。この分業とOCIという共通の窓口が、ランタイムを差し替え可能にし、現代のコンテナ基盤を支えています。
OS Article
コンテナランタイムの内部(runc・OCI仕様)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
コンテナ
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
runc(low-level)はconfig.jsonを読み、clone/unshareで名前空間を作り、cgroupへ登録し、pivot_rootでrootfsを切り替え、capability・seccomp・no_new_privsを設定してからexecveする。状態はOCI state JSONで表現される。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「コンテナ / runc」に近いか確認する。
- 強みである「OCIは「イメージ仕様(layer群+config.jsonのtar)」と「ランタイム仕様(config.jsonで隔離・権限・cgroup・seccompを宣言した展開済みバンドル)」の2本立てで、両者を橋渡しするのがコンテナ起動の全体像。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。