ちょっと話題の記事

“Too Many Requests.” でビルドが失敗する…。AWS CodeBuild で IP ガチャを回避するために Docker Hub ログインしよう!という話

Docker Hubのダウンロード回数制限、もうはじまってたんですね…。
2020.10.10

最近、AWS CodeBuild で Deckerfile をビルドしていると以下のような Too Many Requests. というメッセージでビルドが失敗することがないでしょうか?

error pulling image configuration: toomanyrequests: Too Many Requests. Please see https://docs.docker.com/docker-hub/download-rate-limit/

この原因と対策について本記事にまとめました。

Download Rate Limit(ダウンロード制限)とは?

そもそも何故 Too Many Requests. が最近になって出るようになったかというと、2020年8月に Docker Hub ではコンテナイメージの Pull 回数にレート制限を設けることを発表しています。

レート制限は以下のとおりです。

プラン レート制限
無料プラン(匿名ユーザー) 100 pull/6時間あたり
無料プラン(認証ユーザー) 200 pull/6時間あたり
Pro 無制限
Team 無制限

buildspec.yml 内で docker login -u *** -p *** といった処理をしていなければ、それは匿名ユーザーで利用していることになります。 (ちなみに aws ecr get-login のログイン処理は ECR へのログインであり、Docker Hub のログインではありませんのでお間違えないように)

「匿名ユーザーで使ってるけど 6 時間あたり 100 pull も出来るならウチの環境では十分やなー」

と思ってスルーされた方も少なくないと思いますが、ちょっと待ってください。リンク先のブログで言及されているとおり、匿名ユーザーは IP アドレスに基づいて制限されます。

For anonymous (unauthenticated) users, pull rates are limited based on the individual IP address.

東京リージョンの CodeBuild のグローバル IP は 8 つ

CodeBuild を非 VPC 環境のプロジェクトとして作成している場合、CodeBuild のグローバル IP は共通アドレスが利用されています。

つまり、自分のビルド環境が 6 時間以内で 1 回目の pull だったとしても、そのときに割り当てられていた CodeBuild のグローバル IP が 6時間以内で 101 回目の pull であった場合、そのリクエストは Too Many Requests. になります。

それでは、東京リージョンの CodeBuild ってグローバル IP 何個で回してるのか?というと ip-ranges.json を参照することで判ります。

$ jq .createDate < ip-ranges.json
"2020-10-08-23-41-17"

$ jq -r '.prefixes[] | \
     select(.region=="ap-northeast-1") | \
     select(.service=="CODEBUILD") | \
     .ip_prefix' < ip-ranges.json
13.112.191.184/29

執筆時点の情報では 13.112.191.184/29 ですので、IP レンジとしては 13.112.191.184 ~ 13.112.191.191 です。つまり 8 アドレスということになります。

ビルド処理のなかで curl https://ipconfig.io/ip で確認した結果がこちら。

[Container] 2020/10/09 10:13:29 Running command echo CodeBuild IP...
CodeBuild IP...

[Container] 2020/10/09 10:13:29 Running command curl https://ipconfig.io/ip
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100    15  100    15    0     0     39      0 --:--:-- --:--:-- --:--:--    38
13.112.191.188

10回ほど試してみましたが確認できたのは、13.112.191.18513.112.191.18813.112.191.191 の 3 つでした。いずれも上記レンジ内の IP が表示されていますので、間違いなさそうです。

Download Rate Limit の対策

それでは本題の Download Rate Limit 対策についてですが、私が検討したパターンは以下の 4 つです。

リトライ

先述のとおり、幾つかのグローバル IP がありますので、リトライによって制限を受けないアドレスを引けたならば pull は成功します。しかし毎回変わる保証はありませんので、立て続けに制限を受けているアドレスを引き続けることもありますので、あまり望ましい対策ではないと考えます。

IP ガチャだと理解したうえで、いずれリトライで成功すれば良いと割り切れる環境ならば利用しても良いかとは思います。

ビルドプロジェクトを VPC に接続する

ビルドプロジェクトは VPC に接続することが可能です。この場合、CodeBuild から Docker Hub への接続はあなたの NAT Gateway の EIP になりますので、匿名ユーザーであってもあなたの環境で 6時間あたり 100 pull まで利用できます。

既に NAT Gateway をお持ちの環境であれば VPC 接続で回避するのも良いかと思いますが、このためだけに NAT Gateway を設置するのであればオススメしません。(1つの NAT Gateway 起動料金だけで 30 日で $44.64。2 AZ でおよそ $90 になります)

NAT Gateway のコストを払った匿名ユーザーの 100 pull を勝ち取るくらいなら、Docer Hub の有料プランのほうが安いです。。

ECR にイメージを集約する

ビルド内で Docker Hub から取得しているイメージを事前に ECR に保管し、ECR から取得するようにすれば DockerHub の制限はもう気になりません。

・・・が、ECR のイメージ管理がめちゃ大変になりそうですね。

ビルド内でユーザー認証する

結局のところ認証ユーザーとして利用するのが一番良いと思います。無料プランでもユーザー認証するだけで、送信元 IP で制限されることなく 6 時間あたり 200 pull まで利用できます。

ということで、今回はビルド処理内で Docker Hub にログインする手順をご紹介します。

ビルド処理内で Docker Hub にログイン

事前準備

以下の環境は既にあるものとして進めます。

  • AWS CodeBuild ビルドプロジェクト
    • Dockerfile をビルドし、ECR に push できる適切な IAM がアタッチされている
  • Docker Hub のユーザー ID/Password または AccessToken

ユーザー認証情報の保管

Docker Hub にログインするための ID や Password または AccessToken を保管するには、AWS Secrets Manager か AWS Systems Manager パラメータストア を利用します。パスワードのローテーションを利用しないのであれば無料で利用可能なパラメータストアで十分と思います(KMS の API 呼出料金はいずれも掛かります)

が、今回は AWS Secrets Manager を使った手順にしました。理由は AWS Secrets Manager だとキーバリューが複数登録できるからです。(複数のパラメータストア登録するのが面倒だっただけです、、すみません)

AWS Secrets Manager 管理コンソールから [シークレット] - [新しいシークレットを保存する] を開き、その他のシークレット を選択します。シークレットキー/値 に Docker Hub のユーザー認証情報を登録します。今回は暗号化キーは DefaultEncryptionKey とします。任意の名前、自動ローテーションを無効にして作成します。

上図では usernamepassword としていますが、Docker Hub で Two-Factor Authentication(MFA)を有効にしている場合は、password の代わりに AccessToken を利用してください。

ビルドプロジェクトの IAM 追加

今回は AWS Secrets Manager を利用しますので、ビルドプロジェクトが利用している IAM ロールに secretsmanager:GetSecretValue の権限を追加してください。

ビルドプロジェクトから Secret を参照する

Secret の参照は buildspec.yml の env: で以下のように記述します。DOCKERHUB_USER: の部分が環境変数名になり、arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:docker-hub-eRuNNrSecret-ID です。シークレットには複数のキーバリューがある場合、値を抽出するキーを :username のように指定します。

env:
  secrets-manager:
    DOCKERHUB_USER: arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:docker-hub-eRuNNr:username
    DOCKERHUB_PASS: arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:docker-hub-eRuNNr:password

詳細は公式ガイド(Build specification reference for CodeBuild)を参照ください。

Docker Hub へのログイン

冒頭申し上げたとり aws ecr get-login は ECR へのログインですので、Docker Hub へのログインを別途記述します。Docker Hub で MFA を利用されている場合は DOCKERHUB_PASS を AccessToken の値に置き換えるだけで、ログイン方法としては同じです。

phases:
  pre_build:
    commands:
      # ECR へのログイン 
      - echo Logging in to Amazon ECR...
      - $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION)
      # Docker Hub へのログイン
      - echo Logging in to Docker Hub...
      - echo $DOCKERHUB_PASS | docker login -u $DOCKERHUB_USER --password-stdin
      # コミット ID をイメージタグに設定
      - IMAGE_TAG=$CODEBUILD_RESOLVED_SOURCE_VERSION

このように記述してビルドを実行すると、、

[Container] 2020/10/10 02:39:37 Running command echo Logging in to Docker Hub...
Logging in to Docker Hub...

[Container] 2020/10/10 02:39:37 Running command echo $DOCKERHUB_PASS | docker login -u $DOCKERHUB_USER --password-stdin
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

Login Succeeded と表示されていますので、ログインできていますね!

試しにビルド内で ~/.docker/config.json を参照してみると、以下のように "https://index.docker.io/v1/" が追加されています。

[Container] 2020/10/10 02:39:39 Running command cat ~/.docker/config.json
{
    "auths": {
        "0123456789012.dkr.ecr.ap-northeast-1.amazonaws.com": {
            "auth": "*****"
        },
        "https://index.docker.io/v1/": {
            "auth": "*****"
        }
    },
    "HttpHeaders": {
        "User-Agent": "Docker-Client/19.03.3 (linux)"
    }
}

検証は以上です!

さいごに

CodeBuild で Too Many Requests でエラーになった、というお問い合わせをちょこちょこ見かけるようになったので原因と対策についてまとめました。

匿名ユーザー且つ非VPCのビルドプロジェクトは IP ガチャです。不要なビルド失敗を回避されたい場合は、ユーザー認証するように設定いただくのが無難かと思います。設定も簡単ですので、転ばぬ先の杖としてご検討ください。

以上!大阪オフィスの丸毛(@marumo1981)でした!

サンブル

buildspec.yml
version: 0.2

env:
  secrets-manager:
    DOCKERHUB_USER: arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:docker-hub-eRuNNr:username
    DOCKERHUB_PASS: arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:docker-hub-eRuNNr:password
phases:
  pre_build:
    commands:
      # ECR へのログイン 
      - echo Logging in to Amazon ECR...
      - $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION)
      # DockerHub へのログイン
      - echo Logging in to Docker Hub...
      - echo $DOCKERHUB_PASS | docker login -u $DOCKERHUB_USER --password-stdin
      # コミット ID をイメージタグに設定
      - IMAGE_TAG=$CODEBUILD_RESOLVED_SOURCE_VERSION
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...          
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG      
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG