Dockerとbuildkitでpull through cacheを有効にする検証メモ
業務で運用しているGitHub Actionsのセルフホストランナー用にDockerのpull through cacheを設定するための予備調査メモです。最終的にはクラウド上に構築する予定ですが、まずは原理や設定を理解するためにローカルで構築してみた記録です。
この記事の内容はDocker Desktop + WSL2 + Ubuntu 20.04で動作確認しています。macOSやRancher Desktopなどでも同様の設定は可能だと思いますが、設定の方法やconfigファイルの場所などは異なると思われますので注意してください。
pull through cacheとは
pull through cache あるいはツールやドキュメントによってはmirror registry, registry proxyなどとも呼ばれていることがありますが、これを設定するとイメージをpullする際にDocker Hubと直接通信する前に別のレジストリに問い合わせることが可能になります。そこでイメージが見つかればDocker Hubからpullする代わりに前段のレジストリからpullをしてくれます。逆に見つからなかった場合はフォールバックしてDocker Hubからpullを行ってくれるため、仮にレジストリがダウンしていたりイメージが見つからなかったとしてもpullの最終的な挙動自体は代わりません。名前の通りキャッシュとしての振る舞いですね。
より詳しくは以下の記事などが図付きで説明しているため、こちらが参考になると思います。
参考にした記事
まず公式でmirrorを立てるのと設定の簡易的な説明のドキュメント
registry:2はCNCFに移管されたのでドキュメントはここになった
buildkitはdockerではないので別に設定が必要
docker/setup-buildx-actionを使う場合はGHA側でミラーの設定が必要かもしれない?
Earthlyはまた独自のbuildkitを使っているので別途設定が必要
コンテナ用プライベートレジストリのキャッシュを簡単構築
もう少し実践ぽい設定の紹介
mirror.gcr.ioでpull through cacheを試す
Google Cloudは昔からpull through cacheが可能なDocker Hubのミラーレジストリを mirror.gcr.io で提供してくれています。自分で同様のレジストリを立てる前にまずはmirror.gcr.ioを使わせてもらって設定方法と動作を確認してみました。
docker pull
を対応させる
まずは普通のdocker pull。Docker Desktopでのdaemonの設定でregistry-mirrors
にmirror.gcr.ioを設定し、debug:trueでログも出してみます。
{
"builder": {
"gc": {
"defaultKeepStorage": "20GB",
"enabled": true
}
},
"debug": true,
"experimental": true,
"insecure-registries": [],
"ip": "127.0.0.1",
"registry-mirrors": [
"https://mirror.gcr.io"
]
}
WSL2でログの場所を探すのに苦労しました。
https://forums.docker.com/t/docker-desktop-wsl2-engine-log-location/93259 の場所にもたしかにログは存在していたが自分のマシンだと2021年ぐらいのログだったので場所が古い?
https://docs.docker.com/config/daemon/logs/ 公式では %LOCALAPPDATA%\Docker\log\vm\dockerd.log
と書かれているが %LOCALAPPDATA%
とは・・・?結局、合せ技と勘で /mnt/c/Users/{ユーザー名}/AppData/Local/Docker/log/vm/dockerd.log
にて発見しました。ただwindowsのCドライブに置かれているファイルだからか、WSL2からの tail -f
が効かなかった。
ログを適当にtail -1000ぐらいしてみるとこんな内容が書かれており、mirror.gcr.ioを見ていそうなことが分かります。
[2023-10-22T08:15:35.277045615Z][dockerd][I] time="2023-10-22T08:15:35.276688762Z" level=debug msg=resolving host=mirror.gcr.io
[2023-10-22T08:15:35.277085990Z][dockerd][I] time="2023-10-22T08:15:35.276777808Z" level=debug msg="do request" host=mirror.gcr.io request.header.accept="application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, */*" request.header.user-agent="docker/24.0 go/go1.20.6 git-commit/HEAD kernel/5.15.90.1-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/1.6.21+unknown storage-driver/stargz UpstreamClient(Docker-Client/20.10.21 \\(linux\\))" request.method=HEAD url="https://mirror.gcr.io/v2/library/ubuntu/manifests/20.04?ns=docker.io"
[2023-10-22T08:15:35.771479257Z][dockerd][I] time="2023-10-22T08:15:35.771191128Z" level=debug msg="fetch response received" host=mirror.gcr.io response.header.alt-svc="h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" response.header.content-length=1133 response.header.content-type=application/vnd.oci.image.index.v1+json response.header.date="Sun, 22 Oct 2023 08:15:44 GMT" response.header.docker-content-digest="sha256:ed4a42283d9943135ed87d4ee34e542f7f5ad9ecf2f244870e23122f703f91c2" response.header.docker-distribution-api-version=registry/2.0 response.header.server="Docker Registry" response.header.x-frame-options=SAMEORIGIN response.header.x-xss-protection=0 response.status="200 OK" url="https://mirror.gcr.io/v2/library/ubuntu/manifests/20.04?ns=docker.io"
buildx
(buildkitd)を対応させる
dockerd
の方にpull through cacheを設定したとしてもbuildkitを別コンテナとして立ち上げている場合はbuildkitdにも同様の設定が必要になります。今どきはマルチプラットフォームのイメージをビルドするために docker buildx create
でbuildkitdをコンテナで立ち上げることが多いためですが、このあたりの話は今回の主題ではないため詳しくは割愛します。
公式ドキュメントに従ってbuildkitdの設定ファイルを作ります。
debug = true
[registry."docker.io"]
mirrors = ["mirror.gcr.io"]
次に mirror
という名前でbuildxのビルダーを作成します。
docker buildx create \
--use --bootstrap \
--name mirror \
--driver docker-container \
--config $PWD/buildkitd.toml
FROM alpine
を含む適当なDockerfileを作ってビルド。docker logs buildx_buildkit_mirrored0
でbuildkitdのコンテナのログを見ると確かに mirror.gcr.io という文字列が見えるのでちゃんと読んでくれてそうです。
time="2023-10-22T15:02:30Z" level=debug msg="saved 8orw13cwnlz3hdx8c4tp3dmaa as dockerfile:dockerfile:mirror_registry:bb9a0c004a4ddeef" span="[internal] load build definition from Dockerfile" spanID=5c5c37f11154a20f traceID=154b19d321101ea87727f9bfe85685ee
time="2023-10-22T15:02:30Z" level=debug msg=resolving host=mirror.gcr.io spanID=7d6012cdc1c41fe2 traceID=154b19d321101ea87727f9bfe85685ee
time="2023-10-22T15:02:30Z" level=debug msg="do request" host=mirror.gcr.io request.header.accept="application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, */*" request.header.user-agent=buildkit/v0.12 request.method=HEAD spanID=7d6012cdc1c41fe2 traceID=154b19d321101ea87727f9bfe85685ee url="https://mirror.gcr.io/v2/library/alpine/manifests/latest?ns=docker.io"
time="2023-10-22T15:02:30Z" level=debug msg="fetch response received" host=mirror.gcr.io response.header.alt-svc="h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" response.header.content-length=1638 response.header.content-type=application/vnd.docker.distribution.manifest.list.v2+json response.header.date="Sun, 22 Oct 2023 15:02:40 GMT" response.header.docker-content-digest="sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978" response.header.docker-distribution-api-version=registry/2.0 response.header.server="Docker Registry" response.header.x-frame-options=SAMEORIGIN response.header.x-xss-protection=0 response.status="200 OK" spanID=7d6012cdc1c41fe2 traceID=154b19d321101ea87727f9bfe85685ee url="https://mirror.gcr.io/v2/library/alpine/manifests/latest?ns=docker.io"
time="2023-10-22T15:02:30Z" level=debug msg=resolved desc.digest="sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978" host=mirror.gcr.io spanID=7d6012cdc1c41fe2 traceID=154b19d321101ea87727f9bfe85685ee
time="2023-10-22T15:02:30Z" level=debug msg=fetch digest="sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978" mediatype=application/vnd.docker.distribution.manifest.list.v2+json size=1638 spanID=7d6012cdc1c41fe2 traceID=154b19d321101ea87727f9bfe85685ee
time="2023-10-22T15:02:30Z" level=debug msg="do request" digest="sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978" mediatype=application/vnd.docker.distribution.manifest.list.v2+json request.header.accept="application/vnd.docker.distribution.manifest.list.v2+json, */*" request.header.user-agent=buildkit/v0.12 request.method=GET size=1638 spanID=7d6012cdc1c41fe2 traceID=154b19d321101ea87727f9bfe85685ee url="https://mirror.gcr.io/v2/library/alpine/manifests/sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978?ns=docker.io"
ちなみにbuildkitd.tomlの設定ファイルのリファレンスはこれですが、今回やりたいことに対して他に関係ありそうな設定はなかったです。
自分でpull through cache用のレジストリを立てる
ここまででdockerとbuildkitに正しいpull through cacheの設定ができた場合にログから動作を確認する方法が分かりました。次は実際に自分でレジストリサーバーを立ち上げてmirror.gcr.ioの代わりにそちらを参照させるようにします。
composeでregistry:2を立ち上げる
昔はregistry:2のドキュメントはDocker公式に存在したのですが、今はCNCFに移管されたのでドキュメントはこちらになったようです。これを参考にdocker composeでレジストリを立ち上げます。
こういうcompose.ymlで立ち上げます。
services:
registry:
image: registry:2
restart: always
environment:
- REGISTRY_LOGLEVEL=debug
- REGISTRY_PROXY_REMOTEURL="https://registry-1.docker.io"
- REGISTRY_PROXY_USERNAME=$DOCKER_USER
- REGISTRY_PROXY_PASSWORD=$DOCKER_PAT
ports:
- "50000:5000"
ローカル側のポートを5000ではなく50000にしていることに深い意味はないです。自分は開発用に5000ポートを使うことが多いため、被らないように50000にしました。
docker pull
を自前のレジストリサーバーに対応させる
Docker desktopのdaemonの設定の registry-mirrors
を mirror.gcr.io
から http://172.0.0.1:50000
に変更します。ローカルで立ち上げるため通信がHTTPSではないので insecure-registries
も追加しておきます。
"insecure-registries": [
"http://127.0.0.1:50000"
],
"registry-mirrors": [
"http://127.0.0.1:50000"
]
docker pull node:20-slim
したときのcomposeで立ち上げたコンテナのログ。
GetImageManifest
を127.0.0.1:50000に問い合わせていたり、filesystem.PutContent
でローカルに書き込んでいるのでキャッシュとしての動作をしてくれていることが分かります。
mirror_registry-registry-1 | time="2023-10-22T15:59:42.462089665Z" level=debug msg="authorizing request" go.version=go1.20.8 http.request.host="127.0.0.1:50000" http.request.id=7474f35f-17dc-41f4-94d7-6859c258f0c4 http.request.method=HEAD http.request.remoteaddr="172.19.0.1:58866" http.request.uri="/v2/library/node/manifests/20-slim?ns=docker.io" http.request.useragent="docker/24.0 go/go1.20.6 git-commit/HEAD kernel/5.15.90.1-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/1.6.21+unknown storage-driver/stargz UpstreamClient(Docker-Client/20.10.21 \(linux\))" vars.name="library/node" vars.reference=20-slim
mirror_registry-registry-1 | time="2023-10-22T15:59:42.462992704Z" level=debug msg=GetImageManifest go.version=go1.20.8 http.request.host="127.0.0.1:50000" http.request.id=7474f35f-17dc-41f4-94d7-6859c258f0c4 http.request.method=HEAD http.request.remoteaddr="172.19.0.1:58866" http.request.uri="/v2/library/node/manifests/20-slim?ns=docker.io" http.request.useragent="docker/24.0 go/go1.20.6 git-commit/HEAD kernel/5.15.90.1-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/1.6.21+unknown storage-driver/stargz UpstreamClient(Docker-Client/20.10.21 \(linuxse/base.go" trace.func="github.com/docker/distribution/registry/storage/driver/base.(*Base).Stat" trace.id=a4369b37-687f-44cd-974a-191d5f23bd9d trace.line=155 version=2.8.3
mirror_registry-registry-1 | time="2023-10-22T15:59:36.892183366Z" level=debug msg="filesystem.Stat("/")" go.version=go1.20.8 instance.id=43f17ef7-33e7-41f3-8b5f-3cedf112a931 service=registry trace.duration=37.931µs trace.file="github.com/docker/distribution/registry/storage/driver/base/base.go" trace.func="github.com/docker/distribution/registry/storage/driver/base.(*Base).Stat" trace.id=e97eb0e9-98b6-4f34-982c-a76ffa6fa8a6 trace.line=155 version=2.8.3
mirror_registry-registry-1 | time="2023-10-22T15:59:42.462089665Z" level=debug msg="authorizing request" go.version=go1.20.8 http.request.host="127.0.0.1:50000" http.request.id=7474f35f-17dc-41f4-94d7-6859c258f0c4 http.request.method=HEAD http.request.remoteaddr="172.19.0.1:58866" http.request.uri="/v2/library/node/manifests/20-slim?ns=docker.io" http.request.useragent="docker/24.0 go/go1.20.6 git-commit/HEAD kernel/5.15.90.1-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/1.6.21+unknown storage-driver/stargz UpstreamClient(Docker-Client/20.10.21 \(linux\))" vars.name="library/node" vars.reference=20-slim
mirror_registry-registry-1 | time="2023-10-22T15:59:42.462992704Z" level=debug msg=GetImageManifest go.version=go1.20.8 http.request.host="127.0.0.1:50000" http.request.id=7474f35f-17dc-41f4-94d7-6859c258f0c4 http.request.method=HEAD http.request.remoteaddr="172.19.0.1:58866" http.request.uri="/v2/library/node/manifests/20-slim?ns=docker.io" http.request.useragent="docker/24.0 go/go1.20.6 git-commit/HEAD kernel/5.15.90.1-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/1.6.21+unknown storage-driver/stargz UpstreamClient(Docker-Client/20.10.21 \(linux\))" vars.name="library/node" vars.reference=20-slim
mirror_registry-registry-1 | time="2023-10-22T15:59:43.588262769Z" level=debug msg="filesystem.PutContent("/docker/registry/v2/repositories/library/node/_manifests/tags/20-slim/index/sha256/4fa1430cd19507875e65896fdf3176fc1674bc5bbf51b5f750fa30484885c18d/link")" go.version=go1.20.8 http.request.host="127.0.0.1:50000" http.request.id=7474f35f-17dc-41f4-94d7-6859c258f0c4 http.request.method=HEAD http.request.remoteaddr="172.19.0.1:58866" http.request.uri="/v2/library/node/manifests/20-slim?ns=docker.io" http.request.useragent="docker/24.0 go/go1.20.6 git-commit/HEAD kernel/5.15.90.1-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/1.6.21+unknown storage-driver/stargz UpstreamClient(Docker-Client/20.10.21 \(linux\))" trace.duration=4.852152ms trace.file="github.com/docker/distribution/registry/storage/driver/base/base.go" trace.func="github.com/docker/distribution/registry/storage/driver/base.(*Base).PutContent" trace.id=15453ef3-0344-447d-93b5-ea4be172e72a trace.line=110 vars.name="library/node" vars.reference=20-slim
この状態で再度pullをすれば今度はDocker Hubに問い合わせる必要なくローカルのレジストリからイメージを取得するので早いはずです。ただ、dockerは一度取得したイメージが手元に残っているうちはpullしても実際にはpullされないため、まずは docker image rm node20-slim
で削除しておきます。この状態で再度pullしたときに体感できるぐらい早くなったので、やはりちゃんと動いているようです。
一応コンテナの中に入って中身も調べます。debian
や ubuntu
でも実験していたのですが、ちゃんとpullしてきたイメージの実体のデータが保存されていますね。
$ docker compose exec registry sh
/ # ls /var/lib/registry/docker/registry/v2/repositories/library/
debian node ubuntu
# du -sh /var/lib/registry/docker/
410.1M /var/lib/registry/docker/
buildkitdを自前のレジストリサーバーに対応させる(失敗)
次はdockerと同様にbuildkitdも mirror.gcr.io
から書き換えて以下の設定にしてみましたが、自前のレジストリに接続されていないようでした。よく考えてみたらdocker composeだとdockerのネットワークが別に作成されるのでコンテナの中から127.0.0.1では疎通できない?
debug = true
[registry."docker.io"]
mirrors = ["127.0.0.1:50000"]
http = true
insecure = true
[registry."127.0.0.1:50000"]
http = true
insecure = true
自分も今までdockerのネットワークを意識したことがなかったのですが、どうやら普通に docker run
した場合はbridgeというデフォルトのネットワーク内にコンテナが立ち上がるようです。
一方でdocker composeの場合はbridgeではなく、compose.ymlに書かれているコンテナが全て含まれる専用のネットワークに作成されるのがデフォルトの挙動になっています。つまり、現在はこのようなネットワーク構成になっているため、buildkitdのコンテナからregistryのコンテナに疎通できていない状態と考えられます。
buildkitdとregistryが異なるネットワークに属しているため疎通できない
compose.ymlにネットワークの設定を書くことでbridgeネットワーク内に立ち上げることも可能なようですのでこれで解決できる・・・?
なお、この指定を行った際は、compose のプロジェクト内でのサービス名を指定したサービス間の通信ができなくなることに注意してください。他のサービスとの通信が必要なツールの場合は、この指定は避けます。
記事中に言及されているようにbridgeではコンテナ間の通信を http://registry:5000
のようにサービス名で名前解決できないようです。これはDockerの公式ドキュメントでも説明されています。 --link
を用いれば可能だが、これは古い方法であるとも記載されています。
Containers on the default bridge network can only access each other by IP addresses, unless you use the --link option, which is considered legacy. On a user-defined bridge network, containers can resolve each other by name or alias.
また、bridgeネットワークにコンテナを立ち上げた場合、自分の環境では 172.17.0.0/16
のIPアドレスがコンテナに割り振られるのですが、このIPがコンテナごとに固定されている保証もなさそうです。先ほどのbuilkitdの設定を例えば mirrors = ["172.17.0.1:5000"]
のようにしたとしても自分のPCを再起動した後にIPアドレスが変わっていたら意味がありません。何とかして mirrors = ["registry:5000"]
のような設定でコンテナ間の名前解決をしてもらって疎通できるようにしたいです。
dockerのネットワークについて原理はだいたい分かりました。つまりは以下の図のようにdocker composeで立ち上げるregistry:2
とdocker buildx createで立ち上げるbuildkitd
のそれぞれのコンテナが同じネットワークに所属していれば名前解決が可能になるので疎通できるようになるはずです。
buildkitdとregistryの両方ともcomposeのネットワークに属しているため名前解決と疎通が可能
buildxのドキュメントを調べたところ、--driver-opt
というオプションにnetwork=NETMODE
を渡せるようなので、これでdocker composeのネットワーク内にコンテナを立ち上げることができそうです。
buildkitdを自前のレジストリサーバーに対応させる(リベンジ)
compose.ymlにcontainer_name
を追加してレジストリのコンテナ名を固定し、networks
を追加してネットワーク名も固定します。
services:
registry:
container_name: registry
image: registry:2
restart: always
environment:
- REGISTRY_LOGLEVEL=debug
- REGISTRY_PROXY_REMOTEURL="https://registry-1.docker.io"
- REGISTRY_PROXY_USERNAME=$DOCKER_USER
- REGISTRY_PROXY_PASSWORD=$DOCKER_PAT
- REGISTRY_PROXY_TTL=48h
ports:
- "50000:5000"
networks:
- docker_registry
networks:
docker_registry:
name: docker_registry
buildkitd.tomlは以下のようにレジストリのURLを registry:5000
に変更します。同じネットワークに所属されるのでこれで名前解決できるはずです。
debug = true
[registry."docker.io"]
mirrors = ["registry:5000"]
http = true
insecure = true
[registry."registry:5000"]
http = true
insecure = true
buildkitdを docker_registry
のネットワーク内に立ち上げます。
$ docker buildx create \
--use --bootstrap \
--name mirrored \
--driver docker-container \
--driver-opt network=docker_registry \
--config $PWD/buildkitd.toml
この状態でネットワークを見てみるとちゃんと2つのコンテナが同じネットワークに属しています。
$ docker network inspect docker_registry
# 一部を抜粋
"Containers": {
"1019b9711bc630662f68e372abf31e5e98c4aa403ddf51f4d74a00004d89c136": {
"Name": "registry",
"IPv4Address": "172.18.0.2/16",
},
"fa0382714f0906daad63e47e738a528009c0cb748c135e7a171c2b00de5e94e5": {
"Name": "buildx_buildkit_mirrored0",
"IPv4Address": "172.18.0.3/16",
}
},
docker compose logs -f
でregistry
のログを監視しつつ、適当にalpineを使うビルドコマンドを流してみるとログが大量に流れたので成功してそうです。
$ docker buildx build --load . -f - <<EOF
FROM alpine
RUN echo "hello world"
EOF
buildkit側のログを詳しく見てみます。registry:5000
に対してresolveでdigestを問い合わせ、解決したdigestをfetchしてる様子が分かります。
$ docker logs buildx_buildkit_mirrored0
time="2023-10-23T12:29:09Z" level=debug msg=resolving host="registry:5000" spanID=4e209cd26423471f traceID=0f76e161ee7706ee44328fe9e1967fa2
time="2023-10-23T12:29:09Z" level=debug msg="do request" host="registry:5000" request.header.accept="application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, */*" request.header.user-agent=buildkit/v0.12 request.method=HEAD spanID=4e209cd26423471f traceID=0f76e161ee7706ee44328fe9e1967fa2 url="https://registry:5000/v2/library/alpine/manifests/latest?ns=docker.io"
time="2023-10-23T12:29:11Z" level=debug msg="fetch response received" host="registry:5000" response.header.content-length=1638 response.header.content-type=application/vnd.docker.distribution.manifest.list.v2+json response.header.date="Mon, 23 Oct 2023 12:29:11 GMT" response.header.docker-content-digest="sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978" response.header.docker-distribution-api-version=registry/2.0 response.header.etag="\"sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978\"" response.header.x-content-type-options=nosniff response.status="200 OK" spanID=4e209cd26423471f traceID=0f76e161ee7706ee44328fe9e1967fa2 url="https://registry:5000/v2/library/alpine/manifests/latest?ns=docker.io"
time="2023-10-23T12:29:11Z" level=debug msg=resolved desc.digest="sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978" host="registry:5000" spanID=4e209cd26423471f traceID=0f76e161ee7706ee44328fe9e1967fa2
time="2023-10-23T12:29:11Z" level=debug msg=fetch digest="sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978" mediatype=application/vnd.docker.distribution.manifest.list.v2+json size=1638 spanID=4e209cd26423471f traceID=0f76e161ee7706ee44328fe9e1967fa2
レジストリサーバーに自分のローカルから v2/_catalog
にcurlすることでキャッシュされているイメージ一覧を取得できるので、これでalpineが増えている様子も確認できました。
$ curl http://localhost:50000/v2/_catalog
{"repositories":["cimg/base","library/alpine","library/node"]}
これでdockerに加えてbuildkitでも無事にpull through cacheを使えるようになりました。
レジストリのストレージをminioに変更する
registry:2
のデフォルト設定だとキャッシュとして保存されるイメージはローカルのファイルシステムに保存されます。自分のローカルマシンだけで運用するので使用される容量もたかが知れているので全く問題ないですが、registry:2
はS3に保存する機能もあるようなのでこれも試してみます。ただ今回は実験用なので本物のS3の代わりにS3互換のminioをdocker composeで立ち上げて使用します。
これらのドキュメントを見ながらcompose.ymlに環境変数を追加していきます。ドキュメントに従ってYAMLで設定するとYAML自体をコンテナにマウントさせる手間がかかるため、代わりに環境変数で全て設定します。環境変数で設定する場合は設定のキー名を全てを大文字にした上でYAMLの階層区切りを _
に変換するので、例えば storage.s3.accesskey
に対応する環境変数は STORAGE_S3_ACCESSKEY
となります。
ただし例外として REGISTRY_STORAGE
に s3
という値を設定する必要があります。これを設定しないとfilesystemとs3を両方とも設定している判定となり、storage設定は排他的なので重複はダメというエラーで registry:2
が立ち上がらなくなります。あと、minioにリージョンの概念は無いのですが STORAGE_S3_REGION
は必須なので適当な値を入れます。SECUREとSKIPVERIFYはminioコンテナがHTTPなので必要かと思っていたのですが無くても大丈夫でした(よくわかっていないけど少なくともローカルで使う分にはまあいいか)。
services:
registry:
container_name: registry
image: registry:2
restart: always
environment:
- REGISTRY_LOGLEVEL=debug
- REGISTRY_PROXY_REMOTEURL="https://registry-1.docker.io"
- REGISTRY_PROXY_USERNAME="kesin"
- REGISTRY_PROXY_PASSWORD=$DOCKER_PAT
- REGISTRY_PROXY_TTL=48h
- REGISTRY_STORAGE=s3
- REGISTRY_STORAGE_S3_BUCKET=registry
- REGISTRY_STORAGE_S3_ACCESSKEY=root
- REGISTRY_STORAGE_S3_SECRETKEY=password
- REGISTRY_STORAGE_S3_REGION=minio
- REGISTRY_STORAGE_S3_REGIONENDPOINT=http://minio:9000
# - REGISTRY_STORAGE_S3_SECURE=false
# - REGISTRY_STORAGE_S3_SKIPVERIFY=true
- REGISTRY_STORAGE_S3_LOGLEVEL=debug
ports:
- "50000:5000"
networks:
- docker_registry
minio:
container_name: minio
image: minio/minio
restart: always
command: server --console-address ":9001" /data
environment:
- MINIO_ROOT_USER=root
- MINIO_ROOT_PASSWORD=password
ports:
- "9000:9000"
- "9001:9001"
networks:
- docker_registry
volumes:
- minio_data:/data
networks:
docker_registry:
name: docker_registry
volumes:
minio_data:
まずはminioのバケットを作成する必要があります。docker compose up
でminioが立ち上がったらブラウザからlocalhost:9001でコンソールにアクセスし、registry
という名前でバケットを作成します。自分はブラウザでやりましたが、minioやawsのCLIからバケットを作成しても良いでしょう。
最終的に docker compose logs
で registry
のログを見て起動時にエラーを出さなければ正しく設定できています。docker pull debian
で試してみたところ、registryのログ中にs3aws.PutContentが見えるのでminioに接続できていることが分かります。
registry | time="2023-10-24T00:50:01.973578309Z" level=debug msg=GetImageManifest go.version=go1.20.8 http.request.host="127.0.0.1:50000" http.request.id=9ca8ed6f-fd59-49aa-bdfc-8db8890b3f31 http.request.method=HEAD http.request.remoteaddr="172.18.0.1:56782" http.request.uri="/v2/library/debian/manifests/latest?ns=docker.io" http.request.useragent="docker/24.0 go/go1.20.6 git-commit/HEAD kernel/5.15.90.1-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/1.6.21+unknown storage-driver/stargz UpstreamClient(Docker-Client/20.10.21 \(linux\))" vars.name="library/debian" vars.reference=latest
registry | time="2023-10-24T00:50:03.14447878Z" level=debug msg="s3aws.PutContent("/docker/registry/v2/repositories/library/debian/_manifests/tags/latest/index/sha256/7d3e8810c96a6a278c218eb8e7f01efaec9d65f50c54aae37421dc3cbeba6535/link")" go.version=go1.20.8 http.request.host="127.0.0.1:50000" http.request.id=9ca8ed6f-fd59-49aa-bdfc-8db8890b3f31 http.request.method=HEAD http.request.remoteaddr="172.18.0.1:56782" http.request.uri="/v2/library/debian/manifests/latest?ns=docker.io" http.request.useragent="docker/24.0 go/go1.20.6 git-commit/HEAD kernel/5.15.90.1-microsoft-standard-WSL2 os/linux arch/amd64 containerd-client/1.6.21+unknown storage-driver/stargz UpstreamClient(Docker-Client/20.10.21 \(linux\))" trace.duration=11.454148ms trace.file="github.com/docker/distribution/registry/storage/driver/base/base.go" trace.func="github.com/docker/distribution/registry/storage/driver/base.(*Base).PutContent" trace.id=ffb10e61-2bd4-4b09-b213-e06c08fdc637 trace.line=110 vars.name="library/debian" vars.reference=latest
レジストリにキャッシュされたデータのお掃除(未検証)
自前で立てたレジストリにはキャッシュとしてイメージのデータが保存されていくため、データ量が無限に増え続けます。キャッシュという性質上、新しいイメージに比べて古いイメージデータが今後参照される可能性は低いため、ディスクの節約のために可能ならば古いデータを定期的に削除したいです。
ドキュメントにGarbage collectionのページがあるのでこれっぽいのですが、registry
のコンテナ内に garbage-collect
という別のコマンドが存在しており、それを実行することで削除してくれそう?
ただドキュメント上の deleted
の話がよく分からないのと、設定のリファレンスに時折見える delete
の項目に関係があるのかもよく分からなかったです。さらに言えばファイルシステムではなくS3などの外部ストレージを使用している場合のGCがどうなるのかも一切記載がなかったため、挙動がいまいち分かりません。
別のアプローチとしてminio側のライフサイクル機能で古いデータを削除する方法が可能かもしれません。ライフサイクルを1日で削除の設定で一度実験してみた結果では、一応問題なく再びpull through cacheの挙動でDocker Hubからイメージを取得してキャッシュを保存するという挙動をしてくれました。ただ本当に問題がないかどうかは長期に使ってみないと分からないので、最終的にクラウド上にレジストリサーバーを構築する場合に採用するかどうかは検討中です。
本物のS3ならば容量単価が安いため、リスクを取らずにGCやライフサイクルを一切設定せずに永続的にデータを持ち続けるという選択でもいいかもしれません。
最終的な各種設定ファイル
最終的にこのような設定ファイルになりました。これを使用すれば自分以外の環境でもローカルにレジストリとminioを立ち上げ、docker pull
と docker buildx build
でpull through cacheを使えるようになるはずです。
compose.yml
services:
registry:
container_name: registry
image: registry:2
restart: always
environment:
# - REGISTRY_LOGLEVEL=debug
- REGISTRY_PROXY_REMOTEURL="https://registry-1.docker.io"
- REGISTRY_PROXY_USERNAME=$DOCKER_USER
- REGISTRY_PROXY_PASSWORD=$DOCKER_PAT
- REGISTRY_PROXY_TTL=168h
- REGISTRY_STORAGE=s3
- REGISTRY_STORAGE_S3_BUCKET=registry
- REGISTRY_STORAGE_S3_ACCESSKEY=root
- REGISTRY_STORAGE_S3_SECRETKEY=password
- REGISTRY_STORAGE_S3_REGION=minio
- REGISTRY_STORAGE_S3_REGIONENDPOINT=http://minio:9000
# - REGISTRY_STORAGE_S3_LOGLEVEL=debug
# レイヤーのメタデータをインメモリでキャッシュするらしいが効果のほどはよくわからない
- REGISTRY_STORAGE_CACHE_BLOBDESCRIPTOR=inmemory
ports:
- "50000:5000"
networks:
- docker_registry
# docker composeのデフォルト挙動を忘れたので一応ログの容量にキャップをかけておく
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
minio:
container_name: minio
image: minio/minio
restart: always
command: server --console-address ":9001" /data
environment:
- MINIO_ROOT_USER=root
- MINIO_ROOT_PASSWORD=password
ports:
- "9000:9000"
- "9001:9001"
networks:
- docker_registry
volumes:
- minio_data:/data
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks:
docker_registry:
name: docker_registry
volumes:
minio_data:
buildkitd.toml
# debug = true
[registry."docker.io"]
mirrors = ["registry:5000"]
http = true
insecure = true
[registry."registry:5000"]
http = true
insecure = true
これで自分のローカルでは docker pull
, docker buildx build
のどちらかでpullしてきたイメージはキャッシュされるので、別のツールで同じイメージをpullする場合にもDocker Hubに通信が不要となり高速化されました。
ただ、初回のイメージのpullは自前のレジストリを経由するため、何も設定していなかった頃に比べると逆に若干遅くなりました。そもそもpull through cacheを使わずとも一度pullしていればdockerdやbuildkitdのキャッシュ残っている限りは再度pullしても一瞬で終わりますし、自分しか利用者がいないローカルのレジストリではスケールメリットもありません。
正直に言えば自分も若干期待していたところはあるのですが、残念ながらローカルのdockerを高速化するような効果は見込めないでしょう。今回の記事の内容はあくまでpull through cacheの理解を深めるための検証としての価値しかないと思います。
番外編1 buildkitdのデフォルト設定にpull through cacheを導入する(失敗)
ここから先は番外編なので興味がない方は読み飛ばしてください。
docker buildx create
するたびに --config
で設定ファイルを指定するのは面倒なので、buildkitdのデフォルトのconfigファイルとして設定できるかを試してみました。
The file path is /etc/buildkit/buildkitd.toml for rootful mode, ~/.config/buildkit/buildkitd.toml for rootless mode.
これを見る限り今まで使っていたbuildkitd.tomlをどちらかのパスに配置してやれば使ってくれそうなのですが、自分の環境ではどちらのパスに配置しても有効になってくれませんでした。WSL2なのでdockerdの本体はDocker Desktopが関している別のVMに存在していることが関係しているのでしょうか・・・?
理由は分からないですが、どのみち --driver-opt network=docker_registry
のオプションも必要なので多少の面倒さは受け入れても大差はない、ということで自分のローカル環境では諦めることにしました。
番外編2 Earthlyにpull through cacheを導入する
自分は docker buildx
以外にEarthlyを使うこともあるのでこちらにもpull through cacheを導入します。Earthlyについて興味がある方は過去に紹介記事を書いたのでこちらを御覧ください。
Earthlyもbuildkitdを利用したビルドツールなのですが、buildkitdを独自にForkしたカスタマイズ版を使用しているためかpull through cacheのための設定もbuildkitd.tomlではなく独自のconfigファイルの方に書く必要があるようです。
# ドキュメントのサンプルを引用
global:
buildkit_additional_config: |
[registry."docker.io"]
mirrors = ["192.168.0.80:5000"]
[registry."192.168.0.80:5000"]
insecure = true
サンプルを見る限り、Earthly用の設定のyamlの中にbuildkitd.tomlの設定を無理やり入れるようです。ということはここまで使用してきたbuildkitd.tomlの中身をコピペでいけそうです。
$ cat ~/.earthly/config.yml
global:
buildkit_additional_config: |
[registry."docker.io"]
mirrors = ["registry:5000"]
http = true
insecure = true
[registry."registry:5000"]
http = true
insecure = true
earthly bootstrap
をするとEarthlyが専用のbuildkitdをコンテナで立ち上げてくれるので、どのネットワーク上に立ち上がったのかを確認してみます。やはりbridgeネットワーク上にコンテナが立ち上がっていました。
$ earthly bootstrap
buildkitd | Starting buildkit daemon as a docker container (earthly-buildkitd)...
buildkitd | ...Done
bootstrap | Bootstrapping successful.
$ docker container inspect earthly-buildkitd
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "06558ce17ed3b48138e6d4652f3e9de3cb80a5db37650b789d58332fef9bd029",
"EndpointID": "c471c5fa1eb30b2b26b364577e786831c5fe46ab0a994c18f1ba87b3f8abf5df",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:11:00:02",
"DriverOpts": null
}
}
ということはdocker buildx create
に--driver-opt network=docker_registr
を渡したのと同様にearthly-buildkitdのコンテナを立ち上げる際にオプションを渡してネットワークを変更できれば解決できるはずです。Earthlyのドキュメントを再度見直してみると buildkit_additional_args
というオプションがあったので、これに --network
を渡します。
$ cat ~/.earthly/config.yml
global:
buildkit_additional_args: ["--network", "docker_registry"]
buildkit_additional_config: |
[registry."docker.io"]
mirrors = ["registry:5000"]
http = true
insecure = true
[registry."registry:5000"]
http = true
insecure = true
earthly bootstrap
やり直して、再度コンテナが属しているネットワークを調べてみます。registry
, minio
, buildkitd
と同じネットワークにearthly-buildkitd
も立ち上がったので大丈夫でしょう。
$ docker network inspect docker_registry
"Containers": {
"1b2e5fda5a94c30438df7d84554b790fdbd6e758de0fb9a06ba8f": {
"Name": "buildx_buildkit_mirrored0",
"IPv4Address": "172.18.0.3/16",
},
"c61964704efb464c0c64e6dd6412eb67e2e5ad6fcd94e1db5aa4f": {
"Name": "registry",
"IPv4Address": "172.18.0.2/16",
},
"d78b0ebd1aba99f730d078e5f6d69f4cc0fcb97cb5bb0bc88cab7": {
"Name": "minio",
"IPv4Address": "172.18.0.5/16",
},
"e64911ab6d2da334c3f610ebac33e51219655b007830d65dd7e9d": {
"Name": "earthly-buildkitd",
"IPv4Address": "172.18.0.4/16",
}
},
この状態で適当なEarthfileを用意してビルドし、 registry
のログを見るとちゃんとminioにキャッシュしているログが出ていたのでEarthlyでもpull through cacheの設定ができたようです。
Discussion