Docker コンテナのレイヤー構造をざっくりと理解する。その過程で Docker のイメージ構造(Image Manifest V 2, Schema 2)もながめる。
概要
About storage drivers によると、Docker は、ユニオンファイルシステムを利用し、複数のレイヤーを重ね合わせることでファイルシステムを実現している。
例えば 次のような Dockerfile をビルドして実行し、
# cat Dockerfile
FROM alpine:latest
COPY apple.txt /
RUN touch banana.txt /
# docker build -t sample -f Dockerfile .
Sending build context to Docker daemon 3.072kB
Step 1/3 : FROM alpine:latest
latest: Pulling from library/alpine
540db60ca938: Already exists
Digest: sha256:69e70a79f2d41ab5d637de98c1e0b055206ba40a8145e7bddb55ccc04e13cf8f
Status: Downloaded newer image for alpine:latest
---> 6dbb9cc54074
Step 2/3 : COPY apple.txt /
---> 6ad08ed3b839
Step 3/3 : RUN touch banana.txt /
---> Running in 37ed66a952a2
Removing intermediate container 37ed66a952a2
---> 42d7a98af75f
Successfully built 42d7a98af75f
Successfully tagged sample:latest
# docker run -d sample sleep 1000
e33ab5f4410ebcadf535a589549682f18b7eb908789f68dbda2fd0a9a3a30308
実行したコンテナの GraphDriver の設定を確認すると、ファイルシステムの設定が表示される(今回利用したバージョンだと overlay2 が利用される)。
# docker inspect e33ab5f4410ebcadf535a589549682f18b7eb908789f68dbda2fd0a9a3a30308 | jq ".[0].GraphDriver"
{
"Data": {
"LowerDir": "/var/lib/docker/overlay2/6e7ed57f2937b0bdb3d347a1d63878bc6240dd9a22f21d99ce1f36bf57160978-init/diff:/var/lib/docker/overlay2/c4d7eb4d35150b1300390ff8d43ca8f48d05a216a3bd0d651a4c5f732cd3d3ce/diff:/var/lib/docker/overlay2/cfc311bf4a17198e070699d866ba4075c5d8973a7d5e4829f880517f92cdf25d/diff:/var/lib/docker/overlay2/19610454da2e99c19184a385d76f2256d0da2b7210d0ccbf4edc0b9d438022f9/diff",
"MergedDir": "/var/lib/docker/overlay2/6e7ed57f2937b0bdb3d347a1d63878bc6240dd9a22f21d99ce1f36bf57160978/merged",
"UpperDir": "/var/lib/docker/overlay2/6e7ed57f2937b0bdb3d347a1d63878bc6240dd9a22f21d99ce1f36bf57160978/diff",
"WorkDir": "/var/lib/docker/overlay2/6e7ed57f2937b0bdb3d347a1d63878bc6240dd9a22f21d99ce1f36bf57160978/work"
},
"Name": "overlay2"
各パラメータの意味は、次のとおり。
- LowerDir: レイヤーとして重ね合わせるものを指定する。ReadOnly で良い。
- MergedDir: すべてのレイヤー(LowerDir/UpperDir) を重ね合わせたディレクトリ。コンテナから見えるファイル。
- UpperDir: LowerDir に対する変更を差分で持つディレクトリ。コンテナを停止すると消える。
- WorkDir: overlayfs が動作するときに利用する。
Use the OverlayFS storage driver の図がわかりやすかった。
LowerDir で指定されたディレクトリをみると、レイヤーごとのファイルが確認できる。いちばん下層のレイヤーから、alpine:latest
、COPY apple.txt /
、RUN touch banana.txt /
になっている様子がわかる。
# ls /var/lib/docker/overlay2/19610454da2e99c19184a385d76f2256d0da2b7210d0ccbf4edc0b9d438022f9/diff
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
# ls /var/lib/docker/overlay2/cfc311bf4a17198e070699d866ba4075c5d8973a7d5e4829f880517f92cdf25d/diff
apple.txt
# ls /var/lib/docker/overlay2/c4d7eb4d35150b1300390ff8d43ca8f48d05a216a3bd0d651a4c5f732cd3d3ce/diff
banana.txt
また、dockerd が追加した(と思われる)レイヤーも確認できる。これは設定でコンテナのホスト名や DNS サーバの設定に差し替える機能のために、dockerd が差し込むようす。
# ls /var/lib/docker/overlay2/6e7ed57f2937b0bdb3d347a1d63878bc6240dd9a22f21d99ce1f36bf57160978-init/diff
dev etc
# ls /var/lib/docker/overlay2/6e7ed57f2937b0bdb3d347a1d63878bc6240dd9a22f21d99ce1f36bf57160978-init/diff/etc/
hostname hosts mtab resolv.conf
これを図示すると、次のようになる。
イメージ構造
Docker のイメージ構造は Image Manifest V 2, Schema 2 に定義されている。
イメージは主に 3 種類のファイルから構成されている。
manifest
- 後述する config や layers の位置が記載されたもの。
config
- コンテナ実行に必要な情報が記載されたファイル。例えばコンテナ実行時の Cmd は何、とか、WorkDir は何、とか、hostname は何、とか。
layer
- (tar.gz アーカイブされた)ファイル。レイヤーの実体っぽいもの。
Docker Registry HTTP API V2を利用し、各レイヤーのデータを取得して中身をのぞいてみる。 なお、データ構造とイメージのファイル構造はあまり関係がないので、HTTP による取得方法はまあそういうもんかで流してください。
はじめに、先ほどのイメージを Dockerhub に push する。
# docker tag 42d7a98af75f kimullaa/sample:latest
# docker push kimullaa/sample:latest
# docker rmi -f $(docker images -aq)
manifest
manifest を取得してみる。config と layers のポインタになっているのが分かる。
# token=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:kimullaa/sample:pull" | jq -r '.token')
# curl -s -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -H "Authorization: Bearer $token" "https://registry-1.docker.io/v2/kimullaa/sample/manifests/latest" | jq
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1849,
"digest": "sha256:42d7a98af75fe404665d7c4fb12dca643c7246cdefab2f6124a7e5c6082b4505"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 2811969,
"digest": "sha256:540db60ca9383eac9e418f78490994d0af424aab7bf6d0e47ac8ed4e2e9bcbba"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 109,
"digest": "sha256:c3f2e40648c0e80cf46a41c0d5af743b1528c02af35445ed4add23f7f69b7347"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 97,
"digest": "sha256:4ea489072183231ead86738894a2977745afd7103169f9c328417087062b6dfd"
}
]
}
config
config を取得してみる。こちらも実行時に必要な情報が入ってそうなことが分かる。 なお、このファイルの構造は moby/moby spec/v1.2.md で定義されているっぽい。
# token=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:kimullaa/sample:pull" | jq -r '.token')
# curl -s -L -H 'Accept: application/vnd.docker.container.image.v1+json' -H "Authorization: Bearer $token" "https://registry-1.docker.io/v2/kimullaa/sample/blobs/sha256:42d7a98af75fe404665d7c4fb12dca643c7246cd
efab2f6124a7e5c6082b4505" | jq
{
"architecture": "amd64",
"config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh"
],
"Image": "sha256:6ad08ed3b839dd3a96463078a6397f639c6bd07809a2edcd39ca121f696fdf87",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": null
},
"container": "37ed66a952a20943294885caecbbfd357c1980f00de5b057d78e93b654dc6d4d",
"container_config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"touch banana.txt /"
],
"Image": "sha256:6ad08ed3b839dd3a96463078a6397f639c6bd07809a2edcd39ca121f696fdf87",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": null
},
"created": "2021-06-07T01:07:31.224378431Z",
"docker_version": "20.10.2",
"history": [
{
"created": "2021-04-14T19:19:39.267885491Z",
"created_by": "/bin/sh -c #(nop) ADD file:8ec69d882e7f29f0652d537557160e638168550f738d0d49f90a7ef96bf31787 in / "
},
{
"created": "2021-04-14T19:19:39.643236135Z",
"created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]",
"empty_layer": true
},
{
"created": "2021-06-07T01:07:28.009923628Z",
"created_by": "/bin/sh -c #(nop) COPY file:4610a3197c00b0b8e77052a7da9ec139f7ea4c2bc5729dc4216ae30c25d0a50c in / "
},
{
"created": "2021-06-07T01:07:31.224378431Z",
"created_by": "/bin/sh -c touch banana.txt /"
}
],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:b2d5eeeaba3a22b9b8aa97261957974a6bd65274ebd43e1d81d0a7b8b752b116",
"sha256:641288b48190f3b6cab0f777036c0b8346b6abd2c7ff8bf71b4b7c3cebfd5a11",
"sha256:a51ef6f18be9d4eff3c77e16f184e348cd61a637b6bddc2c2e9f25540c4989fe"
]
}
}
ちなみに、config の情報をもとにコンテナの実行に必要なファイル(oci runtime の config.json)を作成するが、 実装上は OCI の出してる image-spec の構造体に変換されていた(というか OCI の application/vnd.oci.image.config.v1+json
と application/vnd.docker.container.image.v1+json
が同一の処理になっていた)。
参考 containerd/containerd WithImageConfigArgs()
OCI の image-spec は、Image Manifest V 2, Schema 2をベースに策定されたようなので、このファイルについてはそのまま採用されたものと思われる。
参考 OCI Image Support Comes to Open Source Docker Registry
layer
layer を取得してみる。tar.gz を解凍すると、それぞれのレイヤーに必要なファイルが突っ込まれていることが分かる。 これらの各レイヤーのイメージをユニオンファイルシステムで重ねて、ファイルシステムを実現しているっぽい(なお tar を単純に解凍するだけではなく、ストレージドライバがもろもろ処理をするっぽい。例えばファイルの削除を表す whiteout file を tar.gz 内の .wh.
ごとに作成したりする)。
# token=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:kimullaa/sample:pull" | jq -r '.token')
# curl -L -H 'Accept: application/vnd.docker.image.rootfs.diff.tar.gzip' -H "Authorization: Bearer $token" "https://registry-1.docker.io/v2/kimullaa/sample/blobs/sha256:540db60ca9383eac9e418f78490994d0af424aab7bf6d0e47ac8ed4e2e9bcbba" -o layer1.tar.gz
# curl -L -H 'Accept: application/vnd.docker.image.rootfs.diff.tar.gzip' -H "Authorization: Bearer $token" "https://registry-1.docker.io/v2/kimullaa/sample/blobs/sha256:c3f2e40648c0e80cf46a41c0d5af743b1528c02af35445ed4add23f7f69b7347" -o layer2.tar.gz
# curl -L -H 'Accept: application/vnd.docker.image.rootfs.diff.tar.gzip' -H "Authorization: Bearer $token" "https://registry-1.docker.io/v2/kimullaa/sample/blobs/sha256:4ea489072183231ead86738894a2977745afd7103169f9c328417087062b6dfd" -o layer3.tar.gz
# mkdir layer{1..3}
# tar -xf layer1.tar.gz -C layer1
# tar -xf layer2.tar.gz -C layer2
# tar -xf layer3.tar.gz -C layer3
# ls layer1
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
# cat layer1/etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.13.5
PRETTY_NAME="Alpine Linux v3.13"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"
# ls layer{2..3}
layer2:
apple.txt
layer3:
banana.txt
ちなみに URL で指定した sha256:xxx という値は、ファイルを検証可能にするための仕組みらしい。
参考 Digest Parameter
URL で指定した sha256:xxx と、ダウンロードしたファイルの sha256 が一致すれば、問題なくダウンロードできたと判断できる。 試しにダウンロードしたファイルに sha256sum を実行すると、URL で指定した値と一致した。
# sha256sum layer1.tar.gz
540db60ca9383eac9e418f78490994d0af424aab7bf6d0e47ac8ed4e2e9bcbba layer1.tar.gz
# sha256sum layer2.tar.gz
c3f2e40648c0e80cf46a41c0d5af743b1528c02af35445ed4add23f7f69b7347 layer2.tar.gz
# sha256sum layer3.tar.gz
4ea489072183231ead86738894a2977745afd7103169f9c328417087062b6dfd layer3.tar.gz
ちなみにx2、レイヤーのダウンロードを sha256 で指定するということは、レイヤーに含まれるファイルが一緒なら、 異なるイメージだとしても同じ sha256 になり、同じ URL からダウンロードされる。ということは docker registry が持っておく tar.gz は 1 つで済むので、すごいディスクイメージの節約になりそう。
ちなみにx3、FROM で指定した alpine:latest
という情報はコンテナのレイヤー構造に含まれていないので、 ということは、FROM で指定したベースイメージが更新されても、docker build
しないと反映されなさそう。ビルドを自動化する作り込みが必要な様子。
参考 Dockerfile をベースイメージの更新に自動で追従させる - 詩と創作・思索のひろば
参考 Docker Hubの自作イメージを自動アップデートしてリリースする - Qiita