TT Lab
はじめる
学ぶ 学習パス コース

コンテナの内部原理

イメージを解体して覗き込む

TT Labで続きを見る

このラボは本物のVM上で動きます

この箱はPodではなく、KubeVirtが起動した仮想マシンです。Linuxカーネルが別に動き、systemdが実際にサービスを管理し、dockerは模倣ではなく本物のDockerエンジンです。docker runで起動したコンテナは実際にプロセスになり、docker execもdocker logsもそのまま動作します。

以前は、このラボはPodの中で動いていました。カーネルの権限をすべて下ろした箱なので、コンテナを起動するステップが塞がれていて、そのためイメージのアーカイブを直接展開してみるという回り道で学んでいました。もう回り道は必要ありません。

知っておくことが2つあります。

目標

イメージをアーカイブにエクスポートして、中に入っているマニフェストとconfigブロブを直接開いてみて、RootFS.Layers(diff_id)とマニフェストのレイヤーダイジェストが、どのように異なる層の値なのかを、手で確認します。最後に、同じベースから作った2つのイメージが最初のレイヤーを共有していることを、ダイジェストの比較で証明します。

なぜ重要なのか

イメージに関する事故は、ほとんどが「タグを信じたから」起きます。タグは人が付けた名前で、いつでも別のダイジェストを指すように変わります。逆にダイジェストは内容のハッシュなので、同じハッシュなら同じ内容が保証されます。この区別を手で確認しておけば、デプロイのパイプラインで何を固定すべきかが明確になります。もう1つ、マニフェストに書かれたレイヤーのダイジェストは圧縮されたもののハッシュで、configのrootfs.diff_idsは圧縮を展開したtarのハッシュです。2つが違うことを知らないと、「同じイメージなのにハッシュが違う」という思い違いで、長い時間を使います。そのためこのラボは、2つの値をそれぞれ別のファイルから取り出してみます。

ステップ

  1. /root/int3ディレクトリを作成し、alpine:3.20イメージのID(コンテンツのハッシュ)を/root/int3/image-id.txtに保存します。64桁の16進数である必要があり、sha256:のプレフィックスは付いていてもかまいません。
  2. nginx:1.27-alpineのrootfsレイヤーのダイジェストを、1行に1つずつ/root/int3/layers.txtに保存します。すべての行がsha256:で始まる64桁である必要があり、行数が実際のレイヤー数と同じである必要があります。
  3. alpine:3.20イメージをアーカイブにエクスポートして、/root/int3/alpine.tarに保存します。この箱ではdocker saveが動作しないので、skopeo copy oci-archive:/opt/images/alpine_3.20.tar oci-archive:/root/int3/alpine.tar:alpine:3.20を使います。やることは同じです。中にmanifest.json(またはindex.json/oci-layout)と、ハッシュ名のブロブが入っている必要があります。
  4. そのアーカイブからmanifest.jsonを取り出して、/root/int3/manifest.jsonとして保存します。有効なJSONである必要があり、レイヤーの一覧が1つ以上入っている必要があります。
  5. ステップ4のマニフェストのConfigフィールドが指すconfigブロブのファイルを、アーカイブから取り出して、/root/int3/config.jsonとして保存します。このファイルにはarchitectureがあり、rootfs.typeがlayersで、rootfs.diff_idsが1つ以上である必要があります。
  6. skopeoで、ローカルのイメージストレージのnginx:1.27-alpineの情報を照会して、/root/int3/skopeo.jsonに保存します。ストレージが空なので、先にskopeo copy --insecure-policy oci-archive:/opt/images/nginx_1.27-alpine.tar containers-storage:docker.io/library/nginx:1.27-alpineで入れてください。podman loadはこの箱では塞がれていますが、skopeoはネームスペースを作らないので、そのまま動きます。Nameにnginxが含まれ、Digestがsha256:の形式で、Layersが1つ以上である必要があります。
  7. alpine:3.20をベースにして、互いに異なる内容を1層だけ載せたイメージ2つを、labhub/oci:aとlabhub/oci:bのタグでビルドします。2つのイメージの最初のレイヤーのダイジェストは同じで、最後のレイヤーのダイジェストは違う必要があります。
  8. /root/int3/oci.mdに、次の4行を、正確にこの形式で書きます。すべてnginx:1.27-alpineが基準です。
    • layer_count=<레이어 개수>(プレースホルダーはレイヤー数です)
    • architecture=<아키텍처>(プレースホルダーはアーキテクチャです)
    • os=<운영체제>(プレースホルダーはオペレーティングシステムです)
    • first_layer12=<첫 레이어 다이제스트에서 sha256: 을 뗀 앞 12자>(プレースホルダーは、最初のレイヤーのダイジェストからsha256:を除いた先頭12文字です)

参考

イメージIDはそのままコンテンツのハッシュ

/root/int3ディレクトリを作成し、alpine:3.20イメージのID(コンテンツのハッシュ)を/root/int3/image-id.txtに保存します。64桁の16進数である必要があり、sha256:のプレフィックスは付いていてもかまいません。

docker image inspectのIdフィールドが、その値です。sha256:のプレフィックスは、あってもなくてもかまいませんが、後ろの16進数はちょうど64桁である必要があります。タグ名ではなく、ハッシュを保存するステップです。

レイヤーダイジェストの一覧を取り出す

nginx:1.27-alpineのrootfsレイヤーのダイジェストを、1行に1つずつ/root/int3/layers.txtに保存します。すべての行がsha256:で始まる64桁である必要があり、行数が実際のレイヤー数と同じである必要があります。

RootFS.Layersの配列を、1行に1つずつ展開して保存します。jqで配列を展開すると、引用符なしで出てきます。行数が実際のレイヤー数とちょうど同じである必要があるので、空行やヘッダーが混ざってはいけません。

イメージをアーカイブにエクスポートする

alpine:3.20イメージをアーカイブにエクスポートして、/root/int3/alpine.tarに保存します。この箱ではdocker saveが動作しないので、skopeo copy oci-archive:/opt/images/alpine_3.20.tar oci-archive:/root/int3/alpine.tar:alpine:3.20を使います。やることは同じです。中にmanifest.json(またはindex.json/oci-layout)と、ハッシュ名のブロブが入っている必要があります。

コンテナのファイルシステム1枚をエクスポートするコマンドと、レイヤー・タグ・履歴を含むイメージをエクスポートするコマンドは、違います。後者を使って初めて、中にマニフェストが入ります。

アーカイブの中のマニフェストを開く

そのアーカイブからmanifest.jsonを取り出して、/root/int3/manifest.jsonとして保存します。有効なJSONである必要があり、レイヤーの一覧が1つ以上入っている必要があります。

tarアーカイブから、特定のファイルを1つだけ取り出せます。手でJSONを作り出さずに、アーカイブの中に入っているものをそのまま取り出してください。取り出したファイルが有効なJSONで、レイヤーの一覧を含んでいる必要があります。

configブロブとdiff_ids

ステップ4のマニフェストのConfigフィールドが指すconfigブロブのファイルを、アーカイブから取り出して、/root/int3/config.jsonとして保存します。このファイルにはarchitectureがあり、rootfs.typeがlayersで、rootfs.diff_idsが1つ以上である必要があります。

マニフェストのConfigフィールドは、アーカイブの中の別のファイルの名前です。そのファイルをもう一度取り出して初めて、architectureとrootfs.diff_idsが入っている、本物のconfigが出てきます。マニフェスト自体をコピーすると失敗します。

レジストリツールでマニフェストを照会する

skopeoで、ローカルのイメージストレージのnginx:1.27-alpineの情報を照会して、/root/int3/skopeo.jsonに保存します。ストレージが空なので、先にskopeo copy --insecure-policy oci-archive:/opt/images/nginx_1.27-alpine.tar containers-storage:docker.io/library/nginx:1.27-alpineで入れてください。podman loadはこの箱では塞がれていますが、skopeoはネームスペースを作らないので、そのまま動きます。Nameにnginxが含まれ、Digestがsha256:の形式で、Layersが1つ以上である必要があります。

この環境はオフラインなので、リモートのレジストリには出られません。skopeoは、トランスポート(transport)のプレフィックスで対象を選びますが、ローカルのイメージストレージを指すトランスポートを使えば、ネットワークなしで照会できます。出力には、Name、Digest、Layersが一緒に出る必要があります。

ベースレイヤーの共有を証明する

alpine:3.20をベースにして、互いに異なる内容を1層だけ載せたイメージ2つを、labhub/oci:aとlabhub/oci:bのタグでビルドします。2つのイメージの最初のレイヤーのダイジェストは同じで、最後のレイヤーのダイジェストは違う必要があります。

2つのイメージは、同じベースから始まり、互いに異なる内容を1層だけ載せる必要があります。ベースが違うと最初のレイヤーが分かれ、内容が同じだと最後のレイヤーまで同じになって、どちらも失敗します。

OCIメタデータの要約

/root/int3/oci.mdに、次の4行を、正確にこの形式で書きます。すべてnginx:1.27-alpineが基準です。

4行とも키=값の形式(プレースホルダーはキーと値です)で、空白や引用符が混ざってはいけません。first_layer12は、最初のレイヤーのダイジェストからsha256:を除いた先頭12文字です。すべてnginxイメージが基準です。