Docker HubのイメージプルがDownloadRateLimitで失敗する問題を、認証を利用して対処した話

明けましておめでとうございます、ANDPAD SREチームの@DanKadoiです。
AWSサポートの一歩進んだ使い方 ~問い合わせの極意編~ の節はありがとうございました。

今回はDocker Hubのイメージプルについて書こうと思います。

困っていたこと

弊社ではいくつかの用途でDocker Hubからイメージプルしていましたが、主に以下のユースケースでDownlowdRateLimitによる失敗が頻発しました。

  • ECRのPrivateリポジトリでホスティングしたい自前のDockerイメージを、CodeBuildプロジェクトでビルドしているケース
[1/2] FROM docker.io/library/mysql:5.6
resolve docker.io/library/mysql:5.6
resolve docker.io/library/mysql:5.6 3.0s done
ERROR: failed to copy: httpReaderSeeker: failed open: unexpected status code https://registry-1.docker.io/v2/library/mysql/blobs/sha256:44241dbd4d38e96362789edfe0743d6cd2cb81322cd788fd448fcad37d652fec: 429 Too Many Requests
  • CodeBuildプロジェクトのうち、実行環境にDocker Hubでホスティングされているイメージを指定しているケース
BUILD_CONTAINER_UNABLE_TO_PULL_IMAGE: Unable to pull customer's container image.
CannotPullContainerError: Error response from daemon: toomanyrequests: You have reached your pull rate limit.
You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limit
  • Amplifyのビルド実行環境に、Docker Hubでホスティングされているイメージを指定しているケース

弊社ではデプロイの自動化以外にも様々な用途でCodeBuildプロジェクトを活用しており、大部分はSREでメンテナンスしているので失敗する都度SREに問い合わせが来ます。「ちょっと待ってから再実行したら、きっと解消すると思います!!」などといった案内はそう長くは通用しません。 これまでDocker Hubの認証情報をCIツールで使うことをしてこなかったのですが、類似の失敗が多発するようになったので、CodeBuildプロジェクトでイメージプルDownloadRateLimitを回避する方法を検討しました。

Docker HubのDownloadRateLimitについて

CodeBuildプロジェクトのDownloadRateLimitによるビルド失敗が頻発する原因を調べてみると、以下のことがわかりました。

アカウントの種類によって、制限が異なる

https://matsuand.github.io/docs.docker.jp.onthefly/docker-hub/download-rate-limit/
https://www.docker.com/pricing
によると、Docker Hub 上でのDownloadRateLimitはアカウントの種類により、下記に設定されるようです。

ユーザーの種類 pull rate limit
非認証ユーザ 100 pull / 6 hour
無料プラン 200 pull / 6 hour
Proプラン 無制限
Teamプラン 無制限

CodeBuildはDownloadRateLimitに当たりやすい

https://docs.docker.com/docker-hub/download-rate-limit/
によると、 Unauthenticated (anonymous) users will have the limits enforced via IP. と記載の通り、非認証ユーザはIPアドレスによってDownloadRateLimitを受けます。

対して、CodeBuildプロジェクトを非VPC環境で作成している場合、東京リージョンにおいては8通りレンジを持ったグローバルIPの環境が使い回されます。 グローバルIPが他者と被るということは、DownloadRateLimitに到達するキャパシティも共有することとなり、DownloadRateLimitを受けやすくなります。

CodeBuildプロジェクトをVPCに接続することで、グローバルIPをNatGatewayのEIPにすることができます。これを利用すると、非認証ユーザであっても 100 pull / 6 hour まではDownloadRateLimitに当たりません。 この辺りは以下の記事がとても参考になります。 https://dev.classmethod.jp/articles/codebuild-has-to-use-dockerhub-login-to-avoid-ip-gacha/

どのように対処したか

この問題を素早く解決するにあたり、対象のCodeBuildプロジェクトが多岐に渡るため以下を重視しました。

  • CodeBuildプロジェクトを利用している開発者やチームの、運用や手間が変わるような変更はしない
  • CodeBuildプロジェクトで稼働している仕組みを、違うCIツールに置き換えるような大きな変更はしない(ex, 制限が緩和されたCircleCiへの移植
  • 未認証ユーザのままDownloadRateLimitを回避するためのハックをしない(ex, グローバルIPを定期的に変更する何らかの仕組み

上記を満たすため、CodeBuildプロジェクトにおいては、Docker Hubの有償プランの契約をしてDownloadRateLimitの無い認証情報を使って Docker Hubにホスティングされているイメージを利用することとしました。 VPoEの下司さんに相談したところ快諾して頂き、社内稟議もスムーズに通りました。頼りになります。

f:id:d_hack0928:20210125162032p:plain
DownloadRateLimitをお金で回避する図

CodeBuildサービスが提供しているイメージを実行環境に設定することで解決する場合は、それで対処しました

SecretsManagerにDocker Hubの認証情報を登録する

AWSのブログ記事を参考にしました。https://aws.amazon.com/jp/blogs/devops/how-to-use-docker-images-from-a-private-registry-in-aws-codebuild-for-your-build-environment/

まず、SecretsManagerを開いて「新しいシークレットを保存する」を押下し、認証情報をバリューに入れていきます。

f:id:d_hack0928:20210125165504p:plain

ここでは暗号キーはデフォルトを使う前提で進めます。 次へ進んでシークレット名/デスクリプション/タグを任意に設定し、次へ進んで自動ローテーションを無効にします。 最後の確認画面で問題がなければ保存します。保存できたら、作成したSecretのArnを記録してください。

次に、このSecretに対する読み取りアクセスを許可するIAMポリシーを以下のように作成し、CodeBuildプロジェクトに指定しているIAMロールにアタッチします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Sample",
            "Effect": "Allow",
            "Action": "secretsmanager:GetSecretValue",
            "Resource": "arn:aws:secretsmanager:${リージョン}:${アカウント番号}:secret:${シークレット名orワイルドカード}"
        }
    ]
}

CodeBuildプロジェクトでDocker Hubの認証情報を使う

以下2通りのユースケースが想定されますので、1つずつ見ていきます。

1. プロビジョニング時にDocker Hubと認証してイメージプルする

まず、CodeBuildプロジェクトの「環境」を変更します。

f:id:d_hack0928:20210125171208p:plain

次に、イメージの上書きをクリックして、

f:id:d_hack0928:20210125171041p:plain

Docker Hubからプルしたいイメージと、Docker Hubの認証情報が格納されているSecretのArnを入力します。

f:id:d_hack0928:20210125170921p:plain

最後に、環境を更新します。以上です。

注意
CodeBuildのプロビジョニング時にSecretsManagerから認証情報を取得させる際は、Secretには必ずキーをusername, passwordとしてください。 キー名が期待する文字でない場合、CodeBuildのプロビジョニングは以下のようなエラーで失敗します。 f:id:d_hack0928:20210125172743p:plain

2. 実行後に、環境変数経由で認証情報を取得し、docker loginする

まず、CodeBuildプロジェクトの「環境」を変更します。 追加設定のプルダウンから、環境変数の入力フォームを確認します。 Docker Hubの認証情報を、以下の例のようにキーと値に入力し、それぞれタイプをSecretsManagerに設定します。

名前: DOCKERHUB_USER
値: ${Docker Hubの認証情報を格納したSecretのArnに相当する文字列}:username
タイプ: SecretsManager
---
名前: DOCKERHUB_PASS
値: ${Docker Hubの認証情報を格納したSecretのArnに相当する文字列}:password
タイプ: SecretsManager

f:id:d_hack0928:20210125174327p:plain

次に、CodeBuildプロジェクトに指定しているbuildspec.ymlにてDocker Hubにログインするコマンドを追加します。

echo $DOCKERHUB_PASS | docker login -u $DOCKERHUB_USER --password-stdin

最後に、環境を更新します。以上です。

おわりに

いかがでしょうか、CodeBuildプロジェクトでDocker Hubの認証情報を利用するための設定について書いてみました。 無料ユーザの認証情報でも同じ事が出来ますので、DownloadRateLimitによるビルド失敗を解決したい方は是非試してみていただければと思います。

アンドパッドでは新しい仲間を募集しています。
SREも、その他のポジションも、お気軽にエントリーしてください。 engineer.andpad.co.jp hrmos.co