イメージを解体して覗き込む
このラボは本物のVM上で動きます
この箱はPodではなく、KubeVirtが起動した仮想マシンです。Linuxカーネルが別に動き、systemdが実際にサービスを管理し、dockerは模倣ではなく本物のDockerエンジンです。docker runで起動したコンテナは実際にプロセスになり、docker execもdocker logsもそのまま動作します。
以前は、このラボはPodの中で動いていました。カーネルの権限をすべて下ろした箱なので、コンテナを起動するステップが塞がれていて、そのためイメージのアーカイブを直接展開してみるという回り道で学んでいました。もう回り道は必要ありません。
知っておくことが2つあります。
- 最初の起動に1分ほどかかります。VMが起動してDockerをインストールするためです。Podのラボ(通常40秒)より遅くなります。
- ブラウザのプレビューはありません。VMに入ってくる接続は、採点用のポート1つだけが開いています。Webサーバーを起動したなら、VMの中で
curlで確認してください。
目標
イメージをアーカイブにエクスポートして、中に入っているマニフェストとconfigブロブを直接開いてみて、RootFS.Layers(diff_id)とマニフェストのレイヤーダイジェストが、どのように異なる層の値なのかを、手で確認します。最後に、同じベースから作った2つのイメージが最初のレイヤーを共有していることを、ダイジェストの比較で証明します。
なぜ重要なのか
イメージに関する事故は、ほとんどが「タグを信じたから」起きます。タグは人が付けた名前で、いつでも別のダイジェストを指すように変わります。逆にダイジェストは内容のハッシュなので、同じハッシュなら同じ内容が保証されます。この区別を手で確認しておけば、デプロイのパイプラインで何を固定すべきかが明確になります。もう1つ、マニフェストに書かれたレイヤーのダイジェストは圧縮されたもののハッシュで、configのrootfs.diff_idsは圧縮を展開したtarのハッシュです。2つが違うことを知らないと、「同じイメージなのにハッシュが違う」という思い違いで、長い時間を使います。そのためこのラボは、2つの値をそれぞれ別のファイルから取り出してみます。
ステップ
/root/int3ディレクトリを作成し、alpine:3.20イメージのID(コンテンツのハッシュ)を/root/int3/image-id.txtに保存します。64桁の16進数である必要があり、sha256:のプレフィックスは付いていてもかまいません。nginx:1.27-alpineのrootfsレイヤーのダイジェストを、1行に1つずつ/root/int3/layers.txtに保存します。すべての行がsha256:で始まる64桁である必要があり、行数が実際のレイヤー数と同じである必要があります。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)と、ハッシュ名のブロブが入っている必要があります。- そのアーカイブから
manifest.jsonを取り出して、/root/int3/manifest.jsonとして保存します。有効なJSONである必要があり、レイヤーの一覧が1つ以上入っている必要があります。 - ステップ4のマニフェストの
Configフィールドが指すconfigブロブのファイルを、アーカイブから取り出して、/root/int3/config.jsonとして保存します。このファイルにはarchitectureがあり、rootfs.typeがlayersで、rootfs.diff_idsが1つ以上である必要があります。 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つ以上である必要があります。alpine:3.20をベースにして、互いに異なる内容を1層だけ載せたイメージ2つを、labhub/oci:aとlabhub/oci:bのタグでビルドします。2つのイメージの最初のレイヤーのダイジェストは同じで、最後のレイヤーのダイジェストは違う必要があります。/root/int3/oci.mdに、次の4行を、正確にこの形式で書きます。すべてnginx:1.27-alpineが基準です。layer_count=<레이어 개수>(プレースホルダーはレイヤー数です)architecture=<아키텍처>(プレースホルダーはアーキテクチャです)os=<운영체제>(プレースホルダーはオペレーティングシステムです)first_layer12=<첫 레이어 다이제스트에서 sha256: 을 뗀 앞 12자>(プレースホルダーは、最初のレイヤーのダイジェストからsha256:を除いた先頭12文字です)
参考
docker image inspect <이미지> | jq -r '.[0].Id'で、イメージIDを読みます(プレースホルダーはイメージです)。- 配列を行単位に展開するときは、
jq -r '.[0].RootFS.Layers[]'の形を使います。 - アーカイブからファイルを1つだけ取り出すには、
tar xf <아카이브> -C <디렉터리> <파일이름>を使います(プレースホルダーは、アーカイブ、ディレクトリ、ファイル名です)。 - skopeoは
skopeo inspect <전송>:<이미지>の形式で、ローカルのストレージ用のトランスポートはcontainers-storage:です(プレースホルダーは、トランスポートとイメージです)。 - よくある間違い1: ステップ3で
docker exportを使うと、コンテナのファイルシステム1枚だけが出てきて、マニフェストがありません。 - よくある間違い2: ステップ5でマニフェストをそのままコピーすると、
architectureがなくて失敗します。Configが指す別のファイルを取り出す必要があります。 - よくある間違い3: ステップ7で2つのイメージに同じ内容を入れると、レイヤーが完全に同じになって比較になりません。
イメージ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が基準です。
layer_count=<레이어 개수>(プレースホルダーはレイヤー数です)architecture=<아키텍처>(プレースホルダーはアーキテクチャです)os=<운영체제>(プレースホルダーはオペレーティングシステムです)first_layer12=<첫 레이어 다이제스트에서 sha256: 을 뗀 앞 12자>(プレースホルダーは、最初のレイヤーのダイジェストからsha256:を除いた先頭12文字です)
4行とも키=값の形式(プレースホルダーはキーと値です)で、空白や引用符が混ざってはいけません。first_layer12は、最初のレイヤーのダイジェストからsha256:を除いた先頭12文字です。すべてnginxイメージが基準です。