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