- 物理機リージョン/AZ
- ハイパーバイザVM exit で捕まえる
- VMゲストOS
- コンテナnamespace / cgroup今ここ
- プロセスアプリ / DB
コンテナの中身(ファイル)はどう積まれているか。
まず結論
イメージは1枚の塊ではなく、**読み取り専用の差分(レイヤ)の積み重ね**です。OCI image-spec はレイヤを「変更集合」と定義し、各レイヤを**中身のハッシュ(digest)で名指し**します。
帰結は2つ。書き込みは下を書き換えず、上へ**丸ごとコピーしてから**行われること。削除も「消した」印を上に足すだけなので、**消したはずの中身は下のレイヤに残る**ことです。
Dockerfile で `RUN echo secret > /s` の次に `RUN rm /s` と書いてイメージを作りました。配った相手は secret を読めるでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
イメージは差分の積み重ねで、各段は中身のハッシュで呼ばれる
OCI image-spec は、イメージを manifest・image index(任意)・レイヤの集合・configuration と定義します。レイヤについて layer.md はこう書きます。
This document describes how to serialize a filesystem and filesystem changes like removed files into a blob called a layer. One or more layers are applied on top of each other to create a complete filesystem.
レイヤは完成品ではなく変更集合です。 変更は追加・変更・削除の3種だけ。追加と変更は tar の中で同じ形ですが、削除だけは違います。
Removals are represented using "whiteout" file entries
whiteout は「この名前は消えている」と示す空ファイルで、名前は .wh. に消したい名前をつないだもの。削除もまた1枚レイヤを足す操作であって、下を削る操作ではありません。
もう1つの軸が名前の付け方。manifest.md は第一の目標を content-addressable images と述べ、descriptor.md は digest を定義します。
The digest property of a Descriptor acts as a content identifier, enabling content addressability. It uniquely identifies content by taking a collision-resistant hash of the bytes.
値は sha256:6c3c624b58dbbcd3… のように アルゴリズム:符号化された値 の形。中身が名前を決めるので、同じ名前は同じ中身です。
- ・各レイヤは digest で名指しされ、同じ digest なら同じ中身
- ・削除はレイヤを減らさず、上に印を1枚足す
実機: rm したファイルは配布物に残っている
3行の Dockerfile をビルドして開きます。
FROM alpineRUN echo secret > /sRUN rm /s
積まれた層を見ます。
IMAGE CREATED CREATED BY SIZE COMMENT4693b3a34e4b Less than a second ago RUN /bin/sh -c rm /s # buildkit 4.1kB buildkit.dockerfile.v0<missing> Less than a second ago RUN /bin/sh -c echo secret > /s # buildkit 8.19kB buildkit.dockerfile.v0<missing> 3 months ago CMD ["/bin/sh"] 0B buildkit.dockerfile.v0<missing> 3 months ago ADD alpine-minirootfs-3.24.1-aarch64.tar.gz … 9.31MB buildkit.dockerfile.v0
rm の行が 0B ではなく 4.1kB です。 削除がレイヤを1枚増やしています。中身は docker save で取り出して開きます。
--- blobs/sha256/5de55e5ef9c033997441461efe7ba23a986db059c0bb78b38f84ee0d72b99167drwxr-xr-x 0 0 0 0 Jun 14 01:39 bin/lrwxrwxrwx 0 0 0 0 Jun 14 01:39 bin/arch -> /bin/busyboxlrwxrwxrwx 0 0 0 0 Jun 14 01:39 bin/ash -> /bin/busyboxlrwxrwxrwx 0 0 0 0 Jun 14 01:39 bin/base64 -> /bin/busyboxlrwxrwxrwx 0 0 0 0 Jun 14 01:39 bin/bbconfig -> /bin/busybox--- blobs/sha256/4c02f3d296199d54a837123f4ea9bf8c71fe00c381d235ea6936c70bd6170828-rw-r--r-- 0 0 0 7 Sep 16 14:09 s--- blobs/sha256/277cc3b360df047b717b4f9bf3034c63306f4a877e9e45e3ff5860e9187e5151---------- 0 0 0 0 Jan 1 1970 .wh.s
2枚目に 7 バイトの s、3枚目に 0 バイトの .wh.s。s の中身は secret のままです。一方、起動して同じパスを見ると——
ls: /s: No such file or directoryexit=1
実行時に見えないことと、配布物に入っていないことは別です。 whiteout は名前を隠す印で、下のレイヤを書き換えません。Docker の storage drivers の解説も、rm されたディレクトリは前のレイヤで依然として利用可能でイメージの総サイズに加算される、と書きます。
`RUN rm /s` が作るレイヤのサイズは、実機でどうなっていましたか。
書き込みは「先にコピーしてから」行われる
なぜ下は変わらないのか。コンテナのファイルシステムを組み立てる overlayfs について、カーネルの文書はこう説明します。
An overlay filesystem combines two filesystems - an 'upper' filesystem and a 'lower' filesystem. When a name exists in both filesystems, the object in the 'upper' filesystem is visible while the object in the 'lower' filesystem is either hidden or, in the case of directories, merged with the 'upper' object.
同じ名前が両方にあれば上が勝ち、下は隠れる。lower は書き込み可能である必要すらありません。では書き込みが来たら——
When a file in the lower filesystem is accessed in a way that requires write-access, such as opening for write access, changing some metadata etc., the file is first copied from the lower filesystem to the upper filesystem (copy_up).
先にコピーしてから書く。 これが copy-on-write の中身です。削除も同じで、「lower を変更せずに rm と rmdir を支えるため」に upper へ削除の事実を記録する、と文書は述べます。これが overlayfs 側の whiteout です。
粒度も押さえます。overlayfs はファイル単位で働くので、1GB のファイルの 1 バイトを書き換えてもファイル全体が upper へ複製されます。カーネル文書が metacopy を「ファイル全体ではなくメタデータだけをコピーする」機能として別に定義するのも、既定がファイル全体だからです。
別の場所へ書いて、古い方を無効の印で覆う。 第11週『SSDはなぜ書き込みが複雑なのか — 消せる単位が大きすぎる』と同じ形です。あちらでは上書きができないので、別の空きページへ書いて古いページを「無効」と記録していました。
対比: 中身で名指しし、古いものを残す仕組み
Git の内部構造コース『ハッシュ値とblobの対応 — Gitはファイル名ではなく中身を保存する』で同じ骨格を見ました。Git が保存するのは「ファイル」ではなく「中身」で、名前が違っても中身が同じなら blob は1個しか作られません。これが content-addressable と呼ばれる理由でした。
注記はこう続きます。古い blob は書き換えられずそのまま残る、Git のオブジェクトは一度作られたら変更されない(追記のみ)、だから過去のどの時点の中身も取り出せる、と。
削除をコミットしても blob は消えません。 消えるのは新しい木がそれを指さなくなることだけ。イメージの RUN rm と同じ形です。
overlayfs 上のコンテナで、下のレイヤにある 1GB のファイルの 1 バイトだけを書き換えました。何が起きますか。
イメージのレイヤ(OCI + overlayfs)
各レイヤは digest で名指しされた読み取り専用の変更集合。削除は whiteout の印を上に足す操作で、下の中身は残ります。書き込みは copy_up で上へ複製してから行います。
見る場所 /s の状態コンテナ内 ls -l /s No such file or directoryレイヤ2の tar s (7バイト、中身 secret)レイヤ3の tar .wh.s (0バイト)
Git のオブジェクト(Git の内部構造コース)
blob は中身とヘッダだけから機械的に決まるハッシュで名指しされ、一度作られたら変更されません。削除をコミットしても過去の blob は残ります。
見る場所 そのファイルの状態作業ツリー 存在しない最新コミットの木 指していない過去コミットの blob 中身のまま残る
Git の履歴から中身が消えないことと、下のレイヤから中身が消えないこと。共通する構造は何でしょうか。
「削除」が既存の記録に何をしていないかを考えてください。
この章のまとめ
- OCI image-spec はレイヤを変更集合と定義する。変更は追加・変更・削除の3種で、削除だけが whiteout で表される。
- 各レイヤは digest で名指しされる。中身のハッシュなので 同じ名前は同じ中身。手元で計算し直せば改変を検査できる。
- 削除はレイヤを減らさず1枚増やす。 実機では
RUN rm /sの層が 4.1kB、2枚目の層の tar に 7 バイトのs(中身secret)が残った。実行時に見えないことと、配布物に入っていないことは別。 - overlayfs の書き込みは copy_up。下のファイルを上へファイル単位で丸ごと複製してから書くので、下は変わらない。
- OCI の whiteout(
.wh.付き空ファイル)と overlayfs の whiteout(0/0 のキャラクタデバイス等)は別表現。 - タグは manifest への人間可読なポインタで、1つの digest に0個・1個・複数付く。名札は中身を決めない。
次は視点を上げ、これらが載る機械がどこにあるのかを見ます。手を動かすなら、クイズ群「仮想化とコンテナ隔離の原理」の**仮想化V(コンテナランタイムとOCI)と仮想化VI(イメージ層と隔離の限界)**がこの章をそのまま問います。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
OCI image-spec で「レイヤ」はどう定義されていますか。
用語集(4語)
- レイヤ(layer)
- OCI image-spec が定める、ファイルシステムの変更集合を直列化した blob。1枚以上を互いの上に適用して完全なファイルシステムになる。
- digest
- 中身のバイト列の衝突耐性のあるハッシュから作られる内容識別子。中身が同じなら誰が計算しても同じ値になる。
- whiteout
- 「この名前は削除されている」という印。OCI のレイヤ tar では `.wh.` 付きの空ファイル、overlayfs では 0/0 のキャラクタデバイスまたは xattr 付きの空ファイル。同じ語でも表現が違う。
- copy_up
- overlayfs で、下(lower)のファイルが書き込みを要する形で開かれたとき、上(upper)へ複製する処理。ファイル単位で行われる。
出典(一次ソース)
- OCI Image Format Specification — layer.md(変更集合と whiteout) ↗
- OCI Image Format Specification — descriptor.md(digest の定義) ↗
- OCI Image Format Specification — manifest.md(content-addressable が第一目標) ↗
- OCI Distribution Specification — Definitions(Blob / Digest / Tag) ↗
- Linux kernel Documentation — overlayfs.rst(copy_up / whiteout) ↗
- Docker docs — Storage drivers: Images and layers(二次) ↗
- Docker docs — Build cache invalidation(二次) ↗
- Docker Docs — Build cache ↗
- Docker Docs — Building best practices (Pin base image versions) ↗