AWS CodeBuild で Docker Hub の "Too Many Requests. " を回避する検証 | SEEDS Creators' Blog | 株式会社シーズ

AWS CodeBuild で Docker Hub の “Too Many Requests. ” を回避する検証

この記事は AWS & Game Advent Calendar 2020 の9日目の記事です。

クラウド事業部エンジニアの川勝です。

最近AWS Fargate で動くのゲームのバックエンド API を見る機会が増えてきました。その中で実行するコンテナイメージを AWS CodeBuild で作成することも多いのですが “Too Many Requests. “ に遭遇することがありました。
これは Docker Hub の Download rate limit が 2020年10月2日から適応されたことの影響で、その制限がユーザ認証なしだと IP 単位で回数制限がかかるため AWS CodeBuild からの実行では自分が行った回数に関係なく制限に引っかかる場合があるようです。
IPガチャの話は “Too Many Requests.” でビルドが失敗する…。AWS CodeBuild で IP ガチャを回避するために Docker Hub ログインしよう!という話 が詳しいのですが、今回はこれも踏まえて他にも検証しようと思います。

概要

  • AWS CodeBuild でやりたいこと
  • Download rate limit
  • Docker Hub に login
  • AWS CodeBuild の DockerLayerCache
  • doker build –from-cache
  • Amazon ECR Public Gallery

AWS CodeBuild でやりたいこと

API アプリケーションを AWS CodeBuild 上で docker build して Amazon Elastic Container Registry に push する。

Download rate limit

対策していく前にDownload rate limit のおさらい。
FAQ でpull 制限回数を確認しておきましょう。

匿名ユーザIPアドレスごとに6時間あたり100回
認証ユーザユーザごとに6時間あたり200回

(したがって Docker Hub にログインして pull をすればほぼ大丈夫そうではある)

Docker Hub に login

AWS CodeBuild のビルドプロジェクトを AWS マネージメントコンソールから作っていきます。

ソースは GitHub から取得します。今回使用しているリポジトリは以下になります。
https://github.com/kawakattsun/codebuild-docker-rate-limit-verification

ビルドプロジェクトを作成

環境イメージは Amazon Linux 2 の最新で。
Docker のビルドをするので特権付与しておきます。
サービスロールは新しく作成します。

環境

環境変数を設定します。
Docker Hub の認証情報を設定したいのでプレーンテキストではなく AWS SystemsManager パラメータストア から取得するようにします。
ここで作成すると先程作成したサービスロールに AWS SystemsManager パラメータストア から取得するIAM Policyを追加してくれます。

・DOCKER_HUB_ID・・・Docker Hub のユーザIDを
・DOCKER_HUB_PASSWORD・・・Docker Hub のユーザパスワード
・REPOSITORY_URI・・・Amazon ECR のリポジトリURL。シークレットにする必要はあまりないですがついでに登録。

環境変数 – パラーメータの作成
パラメータの作成
パラメータ作成完了

パラメータの作成が完了したら値がセットされていますので buildspec.yml に設定します。中身はこのようになっています。

version: 0.2

env:
  # ここは今回固定値
  variables:
    AWS_DEFAULT_REGION: ap-northeast-1
    REPOSITORY_NAME: codebuild-docker-rate-limit-verification
  # parameter-store セクションに作成したパラメータの値をセット
  parameter-store:
    DOCKER_HUB_ID: /CodeBuild/DOCKER_HUB_ID
    DOCKER_HUB_PASSWORD: /CodeBuild/DOCKER_HUB_PASSWORD
    REPOSITORY_URI: /CodeBuild/REPOSITORY_URI

phases:
  pre_build:
    commands:
      # Amazon ECR へログイン
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | docker login --username AWS --password-stdin ${REPOSITORY_URI}
      # Docker Hub へログイン
      # ちなみに同時にログインしても問題はないようです。
      - echo Logging in to Docker Hub...
      - echo ${DOCKER_HUB_PASSWORD} | docker login -u ${DOCKER_HUB_ID} --password-stdin
  build:
    commands:
      # Docker をビルドしてAmazon ECR へ push
      - echo Docker build and push to Amazon ECR...
      - docker build -t ${REPOSITORY_NAME} .
      - docker tag ${REPOSITORY_NAME}:latest ${REPOSITORY_URI}/${REPOSITORY_NAME}:latest
      - docker push ${REPOSITORY_URI}/${REPOSITORY_NAME}:latest

ビルドプロジェクトを作成したら実行する前に新規に作成されたサービスロールに Amazon ECR リポジトリへログイン、プッシュできる権限を追加します。
新しく以下のような IAM Policy を作成してサービスロールへ追加しました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:BatchCheckLayerAvailability",
                "ecr:CompleteLayerUpload",
                "ecr:InitiateLayerUpload",
                "ecr:PutImage",
                "ecr:UploadLayerPart"
                "ecr:BatchGetImage",
                "ecr:GetDownloadUrlForLayer"
            ],
            "Resource": "arn:aws:ecr:ap-northeast-1:************:repository/codebuild-docker-rate-limit-verification"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken"
            ],
            "Resource": "*"
        }
    ]
}

それでは実行してログを確認します。

[Container] 2020/12/08 11:06:20 Entering phase PRE_BUILD
[Container] 2020/12/08 11:06:20 Running command echo Logging in to Amazon ECR...
Logging in to Amazon ECR...

[Container] 2020/12/08 11:06:20 Running command aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | docker login --username AWS --password-stdin ${REPOSITORY_URI}
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

[Container] 2020/12/08 11:06:29 Running command echo Logging in to Docker Hub...
Logging in to Docker Hub...

[Container] 2020/12/08 11:06:29 Running command echo ${DOCKER_HUB_PASSWORD} | docker login -u ${DOCKER_HUB_ID} --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

[Container] 2020/12/08 11:06:31 Phase complete: PRE_BUILD State: SUCCEEDED

両方ともに Login Succeeded! となっていますね。後続のログはのせていませんがちゃんと push も成功していました。ひとまずこれで rate limit には引っかかりにくくなると思います。

AWS CodeBuild の DockerLayerCache(想定どおりにならず)

次にAWS CodeBuild の DockerLayerCache を試してみます。

AWS CodeBuild ではキャッシュを Amazon S3 または ローカルキャッシュ を設定することができます。
さらにローカルキャッシュには3種類あり、今回は Docker のキャッシュがしたいので DockerLayerCache を選択します。
その他の SourceCache と CustomeCacheは ドキュメント を参考ください。

DockerLayerCacheは以下ように説明されています。

Docker レイヤーキャッシュモードは、既存の Docker レイヤーをキャッシュします。このモードは、大きな Docker イメージを構築または取得するプロジェクトに適しています。そのため、大きな Docker イメージをネットワークからプルすることによって生じるパフォーマンス上の問題を回避できます。

https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-caching.html#caching-local

レイヤーのキャッシュがあれば Docker Hub から pull せずキャッシュを使うことで rate limit の対策になるのでは?と考えました。
キャッシュの注意として一定時間(ドキュメントから具体的な時間は見つけられませんでしたが調べていると1時間後には消えたというのを見かけました)で削除されるようです。
設定は ビルドプロジェクトの アーティファクトの編集 から行います。
キャッシュタイプ: ローカル
DockerLayerCache にチェック
これでOKです。

アーティファクトの編集

ではビルド実行してログを見てみましょう。(以下は連続で2回実行したログ)

[Container] 2020/12/08 12:49:37 Running command docker build -t ${REPOSITORY_NAME} .
Sending build context to Docker daemon  22.02kB

Step 1/7 : FROM golang:1.15.6-buster as build
1.15.6-buster: Pulling from library/golang
756975cb9c7e: Pulling fs layer
d77915b4e630: Pulling fs layer
5f37a0a41b6b: Pulling fs layer
96b2c1e36db5: Pulling fs layer
145393847161: Pulling fs layer
3f8843661db9: Pulling fs layer
218d240e42d4: Pulling fs layer
145393847161: Waiting
3f8843661db9: Waiting
218d240e42d4: Waiting
96b2c1e36db5: Waiting
d77915b4e630: Verifying Checksum
d77915b4e630: Download complete
5f37a0a41b6b: Verifying Checksum
5f37a0a41b6b: Download complete
756975cb9c7e: Verifying Checksum
756975cb9c7e: Download complete
96b2c1e36db5: Verifying Checksum
96b2c1e36db5: Download complete
145393847161: Verifying Checksum
145393847161: Download complete
218d240e42d4: Verifying Checksum
218d240e42d4: Download complete
3f8843661db9: Verifying Checksum
3f8843661db9: Download complete
756975cb9c7e: Pull complete
d77915b4e630: Pull complete
5f37a0a41b6b: Pull complete
96b2c1e36db5: Pull complete
145393847161: Pull complete
3f8843661db9: Pull complete
218d240e42d4: Pull complete
Digest: sha256:ac6f3a6f28356b4a00d0a9c93c7d637eb9b7fac48ce78700ae2a862fc096592b
Status: Downloaded newer image for golang:1.15.6-buster
 ---> 8e2ffcb73e11
Step 2/7 : WORKDIR /go/src/app
 ---> Running in 5b5a2dc4a338
Removing intermediate container 5b5a2dc4a338
 ---> 5b1fca84fda6
Step 3/7 : ADD . /go/src/app
 ---> d68aa9a0492c
Step 4/7 : RUN go build -o /go/bin/app
 ---> Running in fb397271454c
Removing intermediate container fb397271454c
 ---> c5dcdb595e45
Step 5/7 : FROM gcr.io/distroless/base-debian10
latest: Pulling from distroless/base-debian10
be0788dcda67: Pulling fs layer
6c654ddf7049: Pulling fs layer
be0788dcda67: Verifying Checksum
be0788dcda67: Download complete
6c654ddf7049: Verifying Checksum
6c654ddf7049: Download complete
be0788dcda67: Pull complete
6c654ddf7049: Pull complete
Digest: sha256:50944f695c49bd0871c18e1298c283c9e54efa60a68f4ebba113a4d70b8a6c02
Status: Downloaded newer image for gcr.io/distroless/base-debian10:latest
 ---> 779982a7ef90
Step 6/7 : COPY --from=build /go/bin/app /
 ---> 0374ebb59b66
Step 7/7 : CMD ["/app"]
 ---> Running in 3a8173572d22
Removing intermediate container 3a8173572d22
 ---> 1b2930e04bf2
Successfully built 1b2930e04bf2
Successfully tagged codebuild-docker-rate-limit-verification:latest

??あれ?普通に pull してそうだし build も Using cacheとなってませんね…
その後 Dockerfile にキャッシュされそうな apt-get とか足したりもしたのですがキャッシュ使用してくれませんでした…
実行環境のインスタンス?が変わるとローカルにキャッシュは当然ないのでそれかなあ?とか作ったDockerfileがよくない?とかかもしれないですが今回はいい結果が得られませんでした。
効果があるというような記事はみかけましたのでうまく使えると効果がありそうです。

doker build –from-cache(半分うまくいった)

次に前回 push したイメージを pull してきてそれをキャッシュにつかって build というのを試してみたいと思います。
初回以降は Amazon ECR からとってくるので Docker Hub からpull しない、、という想定です。(実際はちょっとちがったのですが)

buiildspec.yml の変更点は以下です。
最新の build したイメージを pull。docker build に –cache-from をつけて build します。

version: 0.2

env:
  variables:
    AWS_DEFAULT_REGION: ap-northeast-1
    REPOSITORY_NAME: codebuild-docker-rate-limit-verification
  parameter-store:
    DOCKER_HUB_ID: /CodeBuild/DOCKER_HUB_ID
    DOCKER_HUB_PASSWORD: /CodeBuild/DOCKER_HUB_PASSWORD
    REPOSITORY_URI: /CodeBuild/REPOSITORY_URI

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | docker login --username AWS --password-stdin ${REPOSITORY_URI}
      - echo Logging in to Docker Hub...
      - echo ${DOCKER_HUB_PASSWORD} | docker login -u ${DOCKER_HUB_ID} --password-stdin
+     - echo Pulling latest docker image...
+     - docker pull ${REPOSITORY_URI}/${REPOSITORY_NAME}:latest
  build:
    commands:
      - echo Docker build and push to Amazon ECR...
-     - docker build -t ${REPOSITORY_NAME} .
+     - docker build --cache-from ${REPOSITORY_URI}/${REPOSITORY_NAME}:latest -t ${REPOSITORY_NAME} .
      - docker tag ${REPOSITORY_NAME}:latest ${REPOSITORY_URI}/${REPOSITORY_NAME}:latest
      - docker push ${REPOSITORY_URI}/${REPOSITORY_NAME}:latest

さて今度の実行の結果は…?

[Container] 2020/12/08 13:28:01 Running command docker build --cache-from ${REPOSITORY_URI}/${REPOSITORY_NAME}:latest -t ${REPOSITORY_NAME} .
Sending build context to Docker daemon  22.53kB

Step 1/7 : FROM golang:1.15.6-buster as build
1.15.6-buster: Pulling from library/golang
756975cb9c7e: Pulling fs layer
d77915b4e630: Pulling fs layer
5f37a0a41b6b: Pulling fs layer
96b2c1e36db5: Pulling fs layer
145393847161: Pulling fs layer
3f8843661db9: Pulling fs layer
218d240e42d4: Pulling fs layer
145393847161: Waiting
3f8843661db9: Waiting
218d240e42d4: Waiting
96b2c1e36db5: Waiting
5f37a0a41b6b: Verifying Checksum
5f37a0a41b6b: Download complete
d77915b4e630: Verifying Checksum
d77915b4e630: Download complete
756975cb9c7e: Download complete
96b2c1e36db5: Verifying Checksum
96b2c1e36db5: Download complete
145393847161: Verifying Checksum
145393847161: Download complete
218d240e42d4: Verifying Checksum
218d240e42d4: Download complete
3f8843661db9: Verifying Checksum
3f8843661db9: Download complete
756975cb9c7e: Pull complete
d77915b4e630: Pull complete
5f37a0a41b6b: Pull complete
96b2c1e36db5: Pull complete
145393847161: Pull complete
3f8843661db9: Pull complete
218d240e42d4: Pull complete
Digest: sha256:ac6f3a6f28356b4a00d0a9c93c7d637eb9b7fac48ce78700ae2a862fc096592b
Status: Downloaded newer image for golang:1.15.6-buster
 ---> 8e2ffcb73e11
Step 2/7 : WORKDIR /go/src/app
 ---> Running in a87e55523dc7
Removing intermediate container a87e55523dc7
 ---> a60cd7cc46ac
Step 3/7 : ADD . /go/src/app
 ---> de43f783cc81
Step 4/7 : RUN go build -o /go/bin/app
 ---> Running in aa848867d6fc
Removing intermediate container aa848867d6fc
 ---> ea449fa0369d
Step 5/7 : FROM gcr.io/distroless/base-debian10
latest: Pulling from distroless/base-debian10
be0788dcda67: Already exists
6c654ddf7049: Already exists
Digest: sha256:50944f695c49bd0871c18e1298c283c9e54efa60a68f4ebba113a4d70b8a6c02
Status: Downloaded newer image for gcr.io/distroless/base-debian10:latest
 ---> 779982a7ef90
Step 6/7 : COPY --from=build /go/bin/app /
 ---> Using cache
 ---> b4e21827fc5c
Step 7/7 : CMD ["/app"]
 ---> Using cache
 ---> 1b2930e04bf2
Successfully built 1b2930e04bf2
Successfully tagged codebuild-docker-rate-limit-verification:latest

うーんと distroless/base-debian10 の方はキャッシュが効いていそうです!
でも golang:1.15.6-buster は普通に pull してそうですね、、、
今回 マルチステージビルド にしているのでビルド用のイメージで実行している方は最終的に破棄されてるはずなので、 push したイメージには存在していない、、、というようなイメージでしょうか。
今回の目的の Docker Hub から pull しない、、、という目的には至らない様子でした。
もう少し大きめの Dockerfile なら速度の効果ありそうです。

Amazon ECR Public Gallery

さて、最終手段(?)でそもそも最初から Docker Hub 以外の選択肢を考えてみます。
ちょうど今月 Amazon ECR Public Gallery がリリースされ Amazon ECR のリポジトリがパブリックに公開可能となりました!
https://aws.amazon.com/jp/blogs/aws/amazon-ecr-public-a-new-public-container-registry/

パブリックなのでサイトはAWSマネージメントコンソールにログインしていなくても閲覧可能です。
https://gallery.ecr.aws/

今回使った distroless はありませんが golang はあるのでこちらを使ってみます。Dockerfile で指定しているイメージを変更します。

- FROM golang:1.15.6-buster as build
+ FROM public.ecr.aws/bitnami/golang:1.14.12-debian-10-r11 as build
WORKDIR /go/src/app
ADD . /go/src/app
RUN go build -o /go/bin/app

FROM gcr.io/distroless/base-debian10
COPY --from=build /go/bin/app /
CMD ["/app"]

結果はまあ問題なし。速度的にもしかしたら同じ AWSネットワーク内で速くなるかな?と思いましたがこれだけだとそんなに変わらない様子でした。
今後の充実に期待ですね。

※追記
上記 rate limit の回避方法みたいな書き方をしていますが、実は Amazon ECR パブリックリポジトリにも制限があります。
2020/12/09現在 *英語* の料金ページに記載されています。(日本語ページにはのっていないので制限ないのかと)
https://aws.amazon.com/ecr/pricing/?nc1=h_ls
Data Transfer OUT なので pull のデータ通信量だと思いますが
匿名ユーザだと 500 GB / Month
AWS アカウントだと 5 TB / Month
となっていました。
匿名ユーザの場合はやはりIPで制限されるのでしょうか。ちょっと記載は見つけれませんでした。

まとめ

当初の “Too Many Requests.” 対策としてはよっぽど高頻度のアクセス出ない限り Docker Hub にログインしておくで問題なさそう。
その他のキャッシュは高頻度でじっこうするプロジェクトでは効果あるはずですが、今回の検証では思った結果が得られませんでした。
そもそも build する docker イメージがもう少し凝ったものだと効果が出たかもしれませんね。
最後に効果が出ていた記事を参考にあげさせて今回の記事は以上といたします。

参考

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

AWS CodeBuildのDocker レイヤーキャッシュのハマったこと