Docker Hub の Rate Limit 問題に対応した話

こんにちは。 SRE の @suzuki-shunsuke です。

Docker Hub から Docker image を Pull する際に Rate Limit に引っかからないように対応した話について書きます。 anonymous user に対する Docker Hub の Rate Limit は段階的に導入されていて、 2020-11-02 9am Pacific Standard Time に完全に導入されるそうです。

https://docs.docker.com/docker-hub/download-rate-limit/

そこで事前に問題が起こらぬよう対応しました。

対応方針

対応方針については大きく 3 つあります。

  1. 同じ image を使いつつ、認証をするようにする
  2. Docker Hub 以外でホスティングされている別の public なイメージを使う
  3. Docker Hub のイメージを別の registry にミラーし、それを使う

対応を始めたのが 2020-10-15 であまり時間もなかったので、一番楽そうな 1 を採用しました。

主に以下の 3 つのパターンに関して対応しました。

  • CircleCI の job で利用している Docker image の pull
  • Docker image を build する際の base image の pull
  • k8s の pod の Docker image の pull

CircleCI の job で利用している Docker image の pull

弊社では CI には主に CircleCI を使っています。

https://circleci.com/docs/ja/2.0/private-images/

公式のドキュメントの通り、ひたすら auth キーを追記しました。 リポジトリの数が多かったので大変でした。 コメントを維持して YAML機械的に編集するのもむずかったりしますし、特に自動化はせずに人力で頑張りました。

CircleCI では当分 Rate Limit がかからないらしい

なお、 CircleCI から Docker Hub の Docker image の pull に関しては当座の間一定の条件付きで Rate Limit がかからないそうです。

https://twitter.com/CircleCIJapan/status/1319453337536720896

ですが、将来的には Rate Limit がかかるかもしれないので対応することが公式に推奨されています。

https://discuss.circleci.com/t/updated-authenticate-with-docker-to-avoid-impact-of-nov-1st-rate-limits/37567/52

Docker image を build する際の base image の pull

build するイメージの push 先が Docker Hub 以外であっても、 base image として Docker Hub の image を使っていれば認証する必要があります。 docker build を実行する前に docker login を実行して認証するようにしました。

k8s の pod の Docker image の pull

PodでImagePullSecretsを指定する に書いてあるとおり、

  1. Docker Hub の Access Token を元に k8s の Secret を生成
  2. Pod ないし ServiceAccount に imagePullSecrets を追記

する必要があります。

弊社では AWS Secrets Manager と aws-secret-operator を利用して Secret を管理しています。 Docker Hub の Access Token についても同様の方法で管理することにしました。

Pod と ServiceAccount のどっちに imagePullSecrets を設定するか

どちらでも結果的には同じことですし、どちらでも良いといえば良いですが、 個人的には Pod に設定するほうが良いのかなと思っています。

というのも imagePullSecrets はどの Docker image を使うかにのみ依存し、 Docker image が指定されている Pod の定義内で管理したほうが管理しやすいからです。 ServiceAccount と Pod を別のファイルで管理していて Pod の Docker image を Docker Hub 以外に変えた場合、 ServiceAccount の imagePullSecrets の修正を忘れる可能性が高いです。

一方で ServiceAccount を複数の Pod で使いまわしている場合、 ServiceAccount で imagePullSecrets を指定したほうが修正は少なくて済むというメリットもあります。

恐らく組織によって ServiceAccount の使われ方や対象の Pod (Deployment, etc) の数は全然違ったりするので、それによっても変わってくるでしょう。

弊社では k8sマニフェストは大きく分けて 2 種類あります。

各プロダクト用のマニフェスト

基本的に各プロダクトチームがオーナーシップを持ち、自分たちで管理しているものです。 ただし、 SRE が修正したりすることもよくあります。

弊社では全プロダクトのアプリケーションのソースコード及び k8sマニフェストを 1 つのリポジトリ(モノレポ) で管理しており、 同じシェルスクリプトkubectl apply を実行してデプロイしています。

これらに関しては、あまり時間がないこともあり、デプロイスクリプトで全ての ServiceAccount に imagePullSecrets を patch するようにしました。

kubectl get sa -o jsonpath --template '{range .items[*]}{.metadata.name}{"\n"}{end}' |
  xargs -n 1 -I{} kubectl patch sa "{}" -p '{"imagePullSecrets": [{"name": "docker-hub"}]}'

このやり方はいちいちマニフェストを修正しなくて良く、漏れも生じないというメリットがある一方、 デプロイが宣言的でなく手続き的になってしまうので、将来的には変えていきたいと思います。

System Component

DataDog Agent や Ingress Controller など、特定のプロダクトに属さず、 cluster scope で動かすものです。 SRE がオーナーシップを持っていて、各プロダクトとは別のリポジトリで管理され、別のシェルスクリプトでデプロイされています。 また、最近では一部のデプロイを ArgoCD による GitOps に移行しています。

こちらに関しては対象が全プロダクトに比べれば少ないことと SRE がオーナーシップを持っていることから 一つ一つ確認してほぼマニフェストに反映させるようにしました。

今後の課題

現在弊社では k8s のデプロイを GitOps に移行している最中です。 今回は時間があまりなかったため、一部でマニフェストに反映させず kubectl によって変更を加えている部分があります。 しかし手続き的なデプロイスクリプトはメンテが大変であり、 GitOps に移行する際にも問題になります。 この問題を解決する方法は主に 2 つあると思っています。

  • マニフェストに反映させる
  • admission webhook などを利用し、自動で imagePullSecrets などが反映されるようにする

マニフェストに反映させる場合、将来的にも漏れが生じないようにするため、 CI でのバリデーションが必要になると思っています。 Conftest によって Docker Hub のイメージだったら imagePullSecrets を必須とするようなバリデーションが考えられます。

後者は k8s に関する応用的な知識が必要だったりして難易度が高めなのですが、上手くやれば漏れなく認証できることが保証されますし、 admission webhook の開発に挑戦する良い機会かなと思います。