ContainerのFilesystemはLayerを重ねていくことで構成していくが、その1つ1つのLayerがどのような内容なのか気になったので調べることにした。
ContainerdのDocsページ(dockerのContainer runtimeのruncを作っているProject)を見るに基本的にはOCIというImage Formatで、Imageの中には以下の2つのjsonファイルが含まれるらしい
- manifestファイル… imageに関する情報が書かれている
- indexファイル…platform(architectureとOSの組み合わせ ex: amd6のLinux)ごとのmanifestファイルのhash情報
このうちmanifestファイルの方にLayerに関する情報が含まれているらしい。ひとまずmanifestファイルの中身を見ることを目的とする。
https://containerd.io/docs/2.3/content-flow/
まずは以下のDockerfileからビルドされるImageを考える。このDockerのFROMやRUNなどのコマンドで作成されるLayerを調べていく。
FROM ubuntu:latest
COPY ./hello.sh hello.sh
RUN sed 's/world/japan/' hello.sh
RUN ./hello.sh
RUN rm hello.sh
CMD tail -F /dev/null
以下のコマンドでビルドする。
cd <Dockerfileがあるディレクトリ>
docker build . -t myhello:1.0
# ビルドできていることを確認
docker images
以下のコマンドで、Imageの内容をtarファイルとして保存することができる。
docker save myhello:1.0 -o myhello.tar
tarを解凍して、treeで中身を見てみる。
-> % tree myhello
myhello
├── blobs
│ └── sha256
│ ├── 01e1d945f346dbd8b1913bffa3c3ea996d913a951183aeb8c0c8965ed0c092b5
│ ├── 2113f8d7eb32748b14581824c1b94cea9ed9a08456312a2e94eddd522d01b927
│ ├── 2fa82af77f18196350fcc21afbd40432eb16229341c651ade5f87b7cda7f09e9
│ ├── 4a7720058461eb4ae40ed203b9874ab3248bd34ffb9948193e99245229fdbd6f
│ ├── 4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1
│ ├── 566ace63b89828ed12cc70881f1cf7437aefc5c2aa8d067878e703a0b87c355a
│ ├── 5be7c372b23fc37302b3c89533b96dff48a15e1fcde7fd7efc91d9010a732893
│ ├── 90d5b56951945c685b4d8461e0dae70a913aa5d786f31ff84d9b5ef24dd0ad50
│ ├── a370b8776a1ae052c3f9200c279fc74714827d8b329e525174c3c60c729f8d9e
│ ├── a6b428496f7545f2fa12f4c5e53d052c63beb73dedd74c0166aa2b96fffaa7f7
│ └── bbafa4d094e00a398161a2b6641c275dc91a9600d54be605d517d4a5afc30656
├── index.json
├── manifest.json
└── oci-layout
3 directories, 14 files
blobディレクトリの中身をひとつずつ開けようとすると、開けたり開けなかったりしたが、01e1d945f346dbd8b1913bffa3c3ea996d913a951183aeb8c0c8965ed0c092b5がテキストデータであり、containerdのページで見たmanifest.jsonの中身そっくりであった。これがPlatformごとのmanifest.jsonなのだろう。
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:90d5b56951945c685b4d8461e0dae70a913aa5d786f31ff84d9b5ef24dd0ad50",
"size": 5461
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:2113f8d7eb32748b14581824c1b94cea9ed9a08456312a2e94eddd522d01b927",
"size": 40728955
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:4a7720058461eb4ae40ed203b9874ab3248bd34ffb9948193e99245229fdbd6f",
"size": 390
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:5be7c372b23fc37302b3c89533b96dff48a15e1fcde7fd7efc91d9010a732893",
"size": 133
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1",
"size": 32
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1",
"size": 32
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:566ace63b89828ed12cc70881f1cf7437aefc5c2aa8d067878e703a0b87c355a",
"size": 78
}
]
}
このjsonのlayerプロパティのdigestがblobのファイル名と一致するから、これらのファイルがLayerについてのデータと考えて良いだろう。
このファイル群はContainerdのmediatypeから察する時tar.gzで保存されているようなので、以下のプログラムでファイル内に圧縮されているファイル名を読み取ってみる。(golang)
package main
import (
"fmt"
"io"
"log"
"archive/tar"
"os"
"compress/gzip"
)
func main() {
path := os.Args[1]
layer, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
gr, err := gzip.NewReader(layer)
if err != nil {
log.Fatal(err)
}
tr := tar.NewReader(gr)
count := 0
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
fmt.Println(hdr.Name)
count++
}
fmt.Fprintln("file num: %v", count)
}
今回Module名はread_layerとした。
実行してみると大量のファイル名が表示された。多分ubuntuに必要がファイル群なのだろう。
-> % go build
-> % ./read_layer <blob dir>/90d5b56951945c685b4d8461e0dae70a913aa5d786f31ff84d9b5ef24dd0ad50
…
…
log/dpkg.log
var/log/lastlog
var/log/wtmp
var/mail/
var/opt/
var/run
var/spool/
var/spool/mail
var/tmp/
file num: 6588%
他のblobデータの中身を見てみると以下のようになった。
- 90d5b56951945c685b4d8461e0dae70a913aa5d786f31ff84d9b5ef24dd0ad50: ubuntuに必要なファイル群
- 4a7720058461eb4ae40ed203b9874ab3248bd34ffb9948193e99245229fdbd6f: .rock/metadata
- 5be7c372b23fc37302b3c89533b96dff48a15e1fcde7fd7efc91d9010a732893 : hello.sh
- 4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1: ファイルなしのLayer
- 4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1: ファイルなしのLayer
- 566ace63b89828ed12cc70881f1cf7437aefc5c2aa8d067878e703a0b87c355a: .wh.hello.sh
Dockerfileの内容と見比べて、よくわからないLayerがいくつか見受けられる。ubuntuに必要なファイル群がLayerに含まれることがわかるが、.rock/metadataというファイルはどこから来たのか。FROMコマンドがLayerを二つ作成しているのだろうか。また、Digestが同じことから、4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1の同一の、ファイルを含んでいないLayerが2回利用されていることがわかる。これは何か。出現箇所から見、RUN sed 's/world/japan/' hello.shとRUN ./hello.shに対応するLayerであり、そもそもファイルを作成していないので、ファイルを含まないLayerが出来上がることは理解できる。ただどちらのコマンド実施するのかの区別がつかない。これではどちらを先に実施するのかがわからない。
.wh.hello.shについては推察がつく。これはRUN rm hello.shに対応するlayerであり、layerを積み重ねていきすでに出来上がったcontainer用のfilesystemから、削除するべきファイルの識別子がこのprefixとして.wh.がついたファイルなのだろう。
ここまででわからないこととしては以下である
- FROMが複数個のLayerを作成しているかどうか
- 複数コマンドが同一のLayarに置き換えられる場合があるが、各々をどのように識別しているのか
調べてみると90d5b56951945c685b4d8461e0dae70a913aa5d786f31ff84d9b5ef24dd0ad50というファイルにこのImageを作成するのに必要なコマンド一覧が載っており、そこから情報を取ることができそうだ。
-> % cat 90d5b56951945c685b4d8461e0dae70a913aa5d786f31ff84d9b5ef24dd0ad50 | jq
{
"architecture": "arm64",
"config": {
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"tail -F /dev/null"
],
"Labels": {
"org.opencontainers.image.created": "2026-04-21T15:27:26.117874+00:00",
"org.opencontainers.image.description": "The Ubuntu container image maintained by Canonical\n\nUbuntu is a Debian-based Linux operating system that runs from the desktop to the cloud, to all your internet connected things.\nIt is the world's most popular operating system across public clouds and OpenStack clouds.\nIt is the number one platform for containers; from Docker to Kubernetes to LXD, Ubuntu can run your containers at scale.\nFast, secure and simple, Ubuntu powers millions of PCs worldwide.\n",
"org.opencontainers.image.title": "ubuntu",
"org.opencontainers.image.version": "26.04"
},
"ArgsEscaped": true
},
"created": "2026-06-30T02:28:49.333798304+09:00",
"history": [
{
"created": "2026-04-21T15:27:25.729590696Z",
"created_by": "umoci raw add-layer --image /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/ubuntu:rockcraft-base /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/.temp_layer.4553.tar --tag 26.04"
},
{
"created": "2026-04-21T15:27:26.085571386Z",
"created_by": "umoci config --image /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/ubuntu:26.04 --clear=config.entrypoint --clear=config.cmd",
"comment": "Set entrypoint",
"empty_layer": true
},
{
"created": "2026-04-21T15:27:26.098832351Z",
"created_by": "umoci config --image /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/ubuntu:26.04 --clear=config.cmd --config.cmd /bin/bash",
"comment": "Set default commands",
"empty_layer": true
},
{
"created": "2026-04-21T15:27:26.110670037Z",
"created_by": "umoci config --image /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/ubuntu:26.04 --config.env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"comment": "Set default PATH for bare-based rock",
"empty_layer": true
},
{
"created": "2026-04-21T15:27:26.122209875Z",
"created_by": "umoci config --image /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/ubuntu:26.04 --clear=config.labels --config.label org.opencontainers.image.version=26.04 --config.label org.opencontainers.image.title=ubuntu --config.label org.opencontainers.image.created=2026-04-21T15:27:26.117874+00:00 --config.label org.opencontainers.image.description=The Ubuntu container image maintained by Canonical\n\nUbuntu is a Debian-based Linux operating system that runs from the desktop to the cloud, to all your internet connected things.\nIt is the world's most popular operating system across public clouds and OpenStack clouds.\nIt is the number one platform for containers; from Docker to Kubernetes to LXD, Ubuntu can run your containers at scale.\nFast, secure and simple, Ubuntu powers millions of PCs worldwide.\n",
"comment": "Set labels",
"empty_layer": true
},
{
"created": "2026-04-21T15:27:26.137480535Z",
"created_by": "umoci config --image /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/ubuntu:26.04 --clear=manifest.annotations --manifest.annotation org.opencontainers.image.version=26.04 --manifest.annotation org.opencontainers.image.title=ubuntu --manifest.annotation org.opencontainers.image.created=2026-04-21T15:27:26.117874+00:00 --manifest.annotation org.opencontainers.image.description=The Ubuntu container image maintained by Canonical\n\nUbuntu is a Debian-based Linux operating system that runs from the desktop to the cloud, to all your internet connected things.\nIt is the world's most popular operating system across public clouds and OpenStack clouds.\nIt is the number one platform for containers; from Docker to Kubernetes to LXD, Ubuntu can run your containers at scale.\nFast, secure and simple, Ubuntu powers millions of PCs worldwide.\n",
"comment": "Set annotations",
"empty_layer": true
},
{
"created": "2026-04-21T15:27:26.156072607Z",
"created_by": "umoci raw add-layer --image /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/ubuntu:26.04 /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/.temp_layer.control_data.4553.tar",
"comment": "Add rock control metadata"
},
{
"created": "2026-06-07T14:29:34.708106501+09:00",
"created_by": "COPY ./hello.sh hello.sh # buildkit",
"comment": "buildkit.dockerfile.v0"
},
{
"created": "2026-06-30T02:01:32.535475982+09:00",
"created_by": "RUN /bin/sh -c sed 's/world/japan/' hello.sh # buildkit",
"comment": "buildkit.dockerfile.v0"
},
{
"created": "2026-06-30T02:01:33.028080274+09:00",
"created_by": "RUN /bin/sh -c ./hello.sh # buildkit",
"comment": "buildkit.dockerfile.v0"
},
{
"created": "2026-06-30T02:28:49.333798304+09:00",
"created_by": "RUN /bin/sh -c rm hello.sh # buildkit",
"comment": "buildkit.dockerfile.v0"
},
{
"created": "2026-06-30T02:28:49.333798304+09:00",
"created_by": "CMD [\"/bin/sh\" \"-c\" \"tail -F /dev/null\"]",
"comment": "buildkit.dockerfile.v0",
"empty_layer": true
}
],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:5cde16a3f2e15fb91f1e3d4002774c76fa956655928f652e9156d5bb63a2ad2f",
"sha256:275233e882bbec676f42159b4909463115d4b5cef13410756a3918679a87cef8",
"sha256:010b4cb38f12101b3503a8418425d475d19a28a6395153ca456954f4c8ed3e9d",
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
"sha256:0f5bef1ec05a01876b2373a074c20b74b6fcda535b3038ac62008b23282b3710"
]
}
}
このファイル内におけるhistoryというのはFROMでベースとしたubuntuイメージを作る際に利用したコマンドも含めたコマンド履歴だろう。なので実際にはFROMを利用すると6つのLayerをRegistryからひっぱってくることになるのだと思う。
historyから見てみると、各履歴の中にempty_layerという項目があり、これがtrueの場合にはblobファイルは作成されないのだろう。emptyの数をhistoryの数からひくとdiff_idsの数と一致する。
同一Layerで示されるコマンドの順序についても、このhistoryの早く出てきた順序で実施すればよさそうだ。
rootfs.diff_idsプロパティというのは調べたところLayerファイルを解凍したもののDigest一覧であるらしい。
参照: https://github.com/opencontainers/image-spec/blob/main/config.md#layer-diffid
またこれら以外にも環境変数一覧やContainerをスタートする際に走らせるコマンドも記録されている。忘れていたが確かにこれらの情報もContainerを動かすのに必要だろう。察するにこのファイルというのContainerを動かすために必要な情報一覧だと思われる。
Layerを重ねていく方法についても以下の通りに行えば良いだろう。
- historyから empty_layer: trueのものを取り除く。
- historyとdiff_idsを上から順番に見ていき、対応するblobを展開してfilesystemに追加/削除を行い、
RUNコマンドなどの場合は指定された処理を実施する。