コンテナイメージのレイヤとコンテンツアドレッシング
同じベースを使うイメージがディスクをほとんど食わず、ビルドが2回目から一瞬で終わる理由がわかります。レイヤのtar差分・ダイジェスト・コンテンツアドレッシングを原理から解説します。
- 1.イメージは順序付きレイヤの集合で、各レイヤはファイルシステム差分をtarアーカイブにしたもの。その内容をSHA-256でハッシュしたダイジェストがレイヤの一意なIDになる。
- 2.ストレージはコンテンツアドレッサブルで、同じ内容のレイヤは同じダイジェストになり物理的に1つだけ保存される。これがイメージ間・レジストリ・ビルドキャッシュでの共有を成立させる。
- 3.起動時は各レイヤを読み取り専用ディレクトリへ展開し、OverlayFSのlowerとして重ねる。レイヤの並びがそのまま下層の重なり順になる。
イメージは「差分の積み重ね」である
コンテナイメージは1枚岩のディスクファイルではなく、変更を積み重ねたレイヤの順序付き集合です。FROM ubuntu の上に apt install、その上にアプリ配置、というように、各ステップが1つのレイヤになります。各レイヤが保持するのは1つ前の状態からのファイルシステム差分だけで、追加・変更されたファイルと、削除を表す目印(whiteout)が入ります。
差分は tar アーカイブ として表現されます。レイヤ=tar という単純さが、後述するハッシュ計算・共有・展開のすべての土台になります。実行時にはこの差分群を下から順に重ね、最終的なルートファイルシステムを合成します。重ね方を担うのが OverlayFS です。
レイヤはまず非圧縮tar(diff)として内容が定義され、これをgzip等で圧縮したものがレジストリ転送・保存の実体です。ハッシュもこの2段階に対応し、圧縮後のblobに対する digest と、非圧縮tarに対する diff_id の2つが存在します(後述)。
ダイジェスト:内容そのものが住所になる
各レイヤ(およびマニフェスト、設定JSON)は、そのバイト列を SHA-256 でハッシュした値で識別されます。OCI仕様ではこれをダイジェストと呼び、sha256: の接頭辞付きで表記します。
sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
└─ レイヤ(または設定/マニフェスト)の内容を SHA-256 した64桁の16進数
ここが核心です。IDは外から付与する名前ではなく、内容から計算される。同じバイト列なら必ず同じダイジェストになり、1バイトでも違えばまったく別のダイジェストになります。これを コンテンツアドレッシング(内容アドレス指定)と呼びます。住所(アドレス)が中身そのものから決まる、という意味です。
この性質から3つの保証が自動的に得られます。
- 完全性検証: ダウンロードしたblobを再ハッシュしてダイジェストと突き合わせれば、改竄・破損を検知できる。署名の土台にもなる。
- 重複排除: 内容が同じなら同じダイジェスト=同じ住所なので、保存もキャッシュも自然に1つにまとまる。
- 不変性: 内容を変えればIDが変わるので、あるダイジェストが指す中身は永久に同じ。タグ(
:latest等)は可変だが、ダイジェスト参照(@sha256:...)は不変。
diff_id は非圧縮レイヤtarのSHA-256で、設定JSON(config)の rootfs.diff_ids に下から順に並びます。digest は圧縮済みblobのSHA-256で、マニフェストの layers[].digest に入ります。前者は「展開後の同一性」、後者は「転送・保存される実体の同一性」を表すため、圧縮方式やレベルが違うと digest は変わっても diff_id は同じ、という状況が起こります。
マニフェスト:ダイジェストを束ねる目次
個々のblobを束ねて1つのイメージにまとめるのが マニフェスト です。マニフェスト自体もJSONのバイト列であり、それをハッシュしたダイジェストでイメージ全体が一意に指せます。構造は「設定への参照」と「レイヤ参照の配列」です。
{
"config": { "digest": "sha256:aaa...", "mediaType": "...config.v1+json" },
"layers": [
{ "digest": "sha256:bbb...", "mediaType": "...layer.v1.tar+gzip" },
{ "digest": "sha256:ccc...", "mediaType": "...layer.v1.tar+gzip" }
]
}
layers 配列は順序が意味を持つ(下から上へ)。設定JSON側の diff_ids も同じ順序で並びます。イメージをpullする側は、まずマニフェストを取得し、知らないダイジェストのblobだけをレジストリへ要求します。すでにローカルにある共通レイヤは再ダウンロードしません。
| 対象 | ハッシュ対象のバイト列 | 格納先 | 可変性 |
|---|---|---|---|
| レイヤ digest | 圧縮済みtar(gzip)のblob | マニフェストの layers[] | 不変 |
| レイヤ diff_id | 非圧縮tarのdiff | config の rootfs.diff_ids | 不変 |
| config digest | 設定JSONのバイト列 | マニフェストの config | 不変 |
| マニフェスト digest | マニフェストJSONのバイト列 | レジストリのインデックス | 不変 |
| タグ(:latest) | (ハッシュではない人間可読名) | レジストリのタグ→digest表 | 可変 |
コンテンツアドレッサブルストレージと共有
ローカルのイメージストア(containerdのcontent storeなど)は、ダイジェストをキーにblobを1つだけ持つ キーバリュー的な格納庫です。同じ住所には同じ中身しか入らないため、保存は自然に重複排除されます。
これが共有の正体です。2つのイメージが同じベースレイヤを使っていれば、そのレイヤのダイジェストは一致し、ディスク上には1コピーだけ存在します。レジストリ側でも同様で、組織内の100個のイメージが共通の FROM を持つなら、その基盤レイヤはレジストリに1回だけ保存・配信されれば足ります。
イメージA: [base] → [runtime] → [appA]
イメージB: [base] → [runtime] → [appB]
▲ ここだけ違う
content store(ダイジェスト→blob、重複なし):
sha256:base... ← AとBが共有(1コピー)
sha256:runtime... ← AとBが共有(1コピー)
sha256:appA... ← Aだけ
sha256:appB... ← Bだけ
この共有は コピーオンライト の発想と同根です。「同じものは1つだけ持ち、違いが出たところだけ分岐する」。レイヤ単位でこれを行うのがイメージ、ファイル単位で行うのがOverlayFSの copy-up です。隔離の文脈は namespaces と cgroups も参照してください。
ビルドキャッシュ:再ビルドが一瞬で終わる仕組み
Dockerfileのビルドでは、各命令が1つのレイヤを生成します。ビルダはレイヤをキャッシュキーで照合し、ヒットすればその命令を再実行せずキャッシュ済みレイヤを使い回します。キャッシュキーは概ね次から決まります。
cache_key(命令) = ハッシュ(
親レイヤのダイジェスト # 直前までの状態が同じか
+ 命令の文字列 # RUN/COPY などの内容
+ COPY/ADD なら投入ファイルの内容ハッシュ
)
ポイントは 親が変われば子も必ずミスする 連鎖性です。あるレイヤがキャッシュミスすると、それ以降の全レイヤも再ビルドされます。だからDockerfileは変更頻度の低い命令を上に、頻繁に変わるものを下に置くのが定石です。依存インストールをソースコピーより前に置く、というよく知られた最適化はこの原理から導かれます。
COPY package.json → RUN install → COPY . . の順にすると、ソースだけ変えた再ビルドでは依存インストールのレイヤがキャッシュヒットし、重い処理がスキップされます。逆に COPY . . を先頭付近に置くと、1行直しただけで全依存が再インストールされます。キャッシュは親レイヤのダイジェストに連鎖する、という1点を押さえれば最適な並びは自明になります。
展開:レイヤをOverlayFSのlowerに重ねる
pullしたレイヤは圧縮tarです。起動前にこれを下から順に読み取り専用ディレクトリへ展開(スナップショット化)します。展開時、tar内のwhiteout(削除マーカー)は対応するOverlayFSの表現へ変換されます。
レイヤ(下→上) 展開先(読み取り専用) OverlayFSマウント
[base] diff_id=b... → /snap/b... ─┐
[runtime] diff_id=r... → /snap/r... ─┤ lowerdir=/snap/a:/snap/r:/snap/b
[app] diff_id=a... → /snap/a... ─┘ (左が上位=appが最優先)
upperdir=/c1/upper(書き込み層)
→ /c1/merged をコンテナのrootfsに
ここでレイヤの順序がそのまま重なり順になります。マニフェストの並び(下から上)が、OverlayFSの lowerdir(左が上位)へ写されます。各 /snap/* は他のコンテナと物理共有され、コンテナ固有なのは upper だけ。実行時にlowerのファイルを書き換えると、ファイル単位の copy-up が走ります。レイヤ=イメージ層の差分、copy-up=実行時の書き込み差分、と差分が2段構えになっているわけです。
この一連の流れ(マニフェスト取得 → blob取得 → スナップショット展開 → OverlayFSマウント → プロセス起動)を統括するのがコンテナランタイムで、low-levelの責務分担は コンテナランタイムの内部 で扱っています。
「なぜ同じベースのイメージはディスクを食わないか」への核心は、(1) レイヤIDが内容のSHA-256(コンテンツアドレッシング)なので同一内容は同一住所になり、(2) content storeがダイジェストをキーに重複排除するため1コピーで済む、の2点です。タグは可変・ダイジェストは不変、digest(圧縮blob)とdiff_id(非圧縮tar)の違いも頻出です。
まとめ
- イメージは順序付きレイヤの集合で、各レイヤは1つ前からのファイルシステム差分をtarにしたもの。下から重ねて最終rootfsを合成する。
- レイヤ・設定・マニフェストは内容のSHA-256(ダイジェスト)で識別される コンテンツアドレッシング。圧縮blobの
digestと非圧縮tarのdiff_idを区別する。内容=住所なので、完全性検証・重複排除・不変性が自動的に成立する。 - ストレージはダイジェストをキーに重複排除し、同一レイヤはイメージ間・レジストリで1コピーだけ共有される。ビルドキャッシュは親レイヤのダイジェストに連鎖し、変更頻度の低い命令を上に置く設計が効く。
- 起動時はレイヤを読み取り専用ディレクトリへ展開し、並び順のまま OverlayFS のlowerに重ねる。共有の根は コピーオンライト、起動の統括は コンテナランタイムの内部 を参照。
OS Article
コンテナイメージのレイヤとコンテンツアドレッシングを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
コンテナ
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
ストレージはコンテンツアドレッサブルで、同じ内容のレイヤは同じダイジェストになり物理的に1つだけ保存される。これがイメージ間・レジストリ・ビルドキャッシュでの共有を成立させる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「コンテナ / OCI」に近いか確認する。
- 強みである「イメージは順序付きレイヤの集合で、各レイヤはファイルシステム差分をtarアーカイブにしたもの。その内容をSHA-256でハッシュしたダイジェストがレイヤの一意なIDになる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。