はじめに
Docker Hubはコンテナレジストリとしては世界で最も広く使われるサービスですが、去年の秋頃からイメージをPullする際に同一IPからの短期的なアクセスに制限がかかるようになりました。
なお、Docker Pullの実際の制限数を知るには以下のリクエストを送ることでレスポンスヘッダーに記載されます。
$ TOKEN=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 4381 0 4381 0 0 4485 0 --:--:-- --:--:-- --:--:-- 4484
$ curl --head -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest
HTTP/1.1 200 OK
content-length: 2782
content-type: application/vnd.docker.distribution.manifest.v1+prettyjws
docker-content-digest: sha256:767a3815c34823b355bed31760d5fa3daca0aec2ce15b217c9cd83229e0e2020
docker-distribution-api-version: registry/2.0
etag: "sha256:767a3815c34823b355bed31760d5fa3daca0aec2ce15b217c9cd83229e0e2020"
date: Thu, 10 Jun 2021 23:52:20 GMT
strict-transport-security: max-age=31536000
ratelimit-limit: 100;w=21600
ratelimit-remaining: 100;w=21600
この場合、21600秒に100回のPullができることになり、6時間に100回まで、ということになります。ちなみにこちらは匿名の場合です。課金済みユーザーでログイン済みの場合ヘッダにratelimit関連のものが含まれないため、無制限であることがわかります。無課金ユーザーでログインの場合は200回まで緩和されるはずですが、今回は未確認。
Kubernetesでの対応
imagePullSecretsを使いたくない...
さて、Kubernetesでアプリケーションを動かすためにはコンテナイメージの取得が必須です。Pod specにてimagePullPolicyをIfNotPresentに設定したり、ImagePullSecretsを設定することで問題を回避することも可能ですが、以下のようなケースではPod specを変えたくない、もしくは変えられないために根本的な対応が行えない場合があります。
- ワークロードがクラスターワイドで、
imagePullSecretを設定するには範囲が大きすぎる - ピュアなKubernetes実装を使っておらず、複数のPodを指定するのが大変かつ、コンポーネントのプロビジョンが自動化されているためにPod specを動的に変えるのが難しい
こうした場合、各ノードのkubeletがイメージをPullする場合にログイン情報を利用するように対応したいのですが、純粋にDocker Loginを行った上でDocker PullをしてもKubernetesではそのPullしたイメージを認識してくれません。これはKubernetesのランタイムインターフェースであるCRIがKubeletからの命令を受けてイメージを管理する時に使うimage serviceのnamespaceと、Dockerが内部的に使うimage serviceの名前空間が異なることが原因で、イメージを読み込むには手動であれこれ手を加える必要があり、面倒です。また、crictlにはlogin interfaceがないため、Kubernetesと同じcontextでイメージを管理する際にCLIを使うのは現実的ではありません。
kubeletが実はDocker credentialを読み取れる実装になっていた
最も確実なのは、Pod specではなくkubeletからイメージレジストリの認証情報を読み取り、CRIにわたすやり方ですが、kubeletには以下のような実装が入っていることがわかりました。
Dockerはプライベートレジストリとの通信に使う鍵を$HOME/.dockercfgまたは$HOME/.docker/config.jsonに保存します。このファイルを以下のいずれかに保存すると、kubeletはイメージPull時に認証プロバイダーとしてその情報を参照します。
{--root-dir:-/var/lib/kubelet}/config.json{cwd of kubelet}/config.json${HOME}/.docker/config.json/.docker/config.json{--root-dir:-/var/lib/kubelet}/.dockercfg{cwd of kubelet}/.dockercfg${HOME}/.dockercfg/.dockercfg
そうなの!?こんなん気づかんやろ!!って思ったんですが、以下のようにやってみたらrate limitから開放されてすんなり構築が完了してしまいました。
# docker login # mkdir /.docker # cp $HOME/.docker/config.json /.docker/
なお、当然ながらこの作業は該当する全ノードにやる必要があるため、一時的な利用のみに留めたい場合はあとで明示的に削除する必要があります。お気をつけください。
それでは、今日はこのへんで。