hero_picture
Cover Image for AWS CodeBuild で Docker Hub の "Too Many Requests. " を回避する検証

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 でやりたいこと

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 に設定します。中身はこのようになっています。

1version: 0.2
2
3env:
4  # ここは今回固定値
5  variables:
6    AWS_DEFAULT_REGION: ap-northeast-1
7    REPOSITORY_NAME: codebuild-docker-rate-limit-verification
8  # parameter-store セクションに作成したパラメータの値をセット
9  parameter-store:
10    DOCKER_HUB_ID: /CodeBuild/DOCKER_HUB_ID
11    DOCKER_HUB_PASSWORD: /CodeBuild/DOCKER_HUB_PASSWORD
12    REPOSITORY_URI: /CodeBuild/REPOSITORY_URI
13
14phases:
15  pre_build:
16    commands:
17      # Amazon ECR へログイン
18      - echo Logging in to Amazon ECR...
19      - aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | docker login --username AWS --password-stdin ${REPOSITORY_URI}
20      # Docker Hub へログイン
21      # ちなみに同時にログインしても問題はないようです。
22      - echo Logging in to Docker Hub...
23      - echo ${DOCKER_HUB_PASSWORD} | docker login -u ${DOCKER_HUB_ID} --password-stdin
24  build:
25    commands:
26      # Docker をビルドしてAmazon ECR へ push
27      - echo Docker build and push to Amazon ECR...
28      - docker build -t ${REPOSITORY_NAME} .
29      - docker tag ${REPOSITORY_NAME}:latest ${REPOSITORY_URI}/${REPOSITORY_NAME}:latest
30      - docker push ${REPOSITORY_URI}/${REPOSITORY_NAME}:latest

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

新しく以下のような IAM Policy を作成してサービスロールへ追加しました。

1{
2    "Version": "2012-10-17",
3    "Statement": [
4        {
5            "Effect": "Allow",
6            "Action": [
7                "ecr:BatchCheckLayerAvailability",
8                "ecr:CompleteLayerUpload",
9                "ecr:InitiateLayerUpload",
10                "ecr:PutImage",
11                "ecr:UploadLayerPart"
12                "ecr:BatchGetImage",
13                "ecr:GetDownloadUrlForLayer"
14            ],
15            "Resource": "arn:aws:ecr:ap-northeast-1:************:repository/codebuild-docker-rate-limit-verification"
16        },
17        {
18            "Effect": "Allow",
19            "Action": [
20                "ecr:GetAuthorizationToken"
21            ],
22            "Resource": "*"
23        }
24    ]
25}

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

1[Container] 2020/12/08 11:06:20 Entering phase PRE_BUILD
2[Container] 2020/12/08 11:06:20 Running command echo Logging in to Amazon ECR...
3Logging in to Amazon ECR...
4
5[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}
6WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
7Configure a credential helper to remove this warning. See
8https://docs.docker.com/engine/reference/commandline/login/#credentials-store
9
10Login Succeeded
11
12[Container] 2020/12/08 11:06:29 Running command echo Logging in to Docker Hub...
13Logging in to Docker Hub...
14
15[Container] 2020/12/08 11:06:29 Running command echo ${DOCKER_HUB_PASSWORD} | docker login -u ${DOCKER_HUB_ID} --password-stdin
16WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
17Configure a credential helper to remove this warning. See
18https://docs.docker.com/engine/reference/commandline/login/#credentials-store
19
20Login Succeeded
21
22[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 イメージをネットワークからプルすることによって生じるパフォーマンス上の問題を回避できます。

レイヤーのキャッシュがあれば Docker Hub から pull せずキャッシュを使うことで rate limit の対策になるのでは?と考えました。

キャッシュの注意として一定時間(ドキュメントから具体的な時間は見つけられませんでしたが調べていると1時間後には消えたというのを見かけました)で削除されるようです。

設定は ビルドプロジェクトの アーティファクトの編集 から行います。

キャッシュタイプ: ローカル

DockerLayerCache にチェック

これでOKです。

アーティファクトの編集
アーティファクトの編集

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

1[Container] 2020/12/08 12:49:37 Running command docker build -t ${REPOSITORY_NAME} .
2Sending build context to Docker daemon  22.02kB
3
4Step 1/7 : FROM golang:1.15.6-buster as build
51.15.6-buster: Pulling from library/golang
6756975cb9c7e: Pulling fs layer
7d77915b4e630: Pulling fs layer
85f37a0a41b6b: Pulling fs layer
996b2c1e36db5: Pulling fs layer
10145393847161: Pulling fs layer
113f8843661db9: Pulling fs layer
12218d240e42d4: Pulling fs layer
13145393847161: Waiting
143f8843661db9: Waiting
15218d240e42d4: Waiting
1696b2c1e36db5: Waiting
17d77915b4e630: Verifying Checksum
18d77915b4e630: Download complete
195f37a0a41b6b: Verifying Checksum
205f37a0a41b6b: Download complete
21756975cb9c7e: Verifying Checksum
22756975cb9c7e: Download complete
2396b2c1e36db5: Verifying Checksum
2496b2c1e36db5: Download complete
25145393847161: Verifying Checksum
26145393847161: Download complete
27218d240e42d4: Verifying Checksum
28218d240e42d4: Download complete
293f8843661db9: Verifying Checksum
303f8843661db9: Download complete
31756975cb9c7e: Pull complete
32d77915b4e630: Pull complete
335f37a0a41b6b: Pull complete
3496b2c1e36db5: Pull complete
35145393847161: Pull complete
363f8843661db9: Pull complete
37218d240e42d4: Pull complete
38Digest: sha256:ac6f3a6f28356b4a00d0a9c93c7d637eb9b7fac48ce78700ae2a862fc096592b
39Status: Downloaded newer image for golang:1.15.6-buster
40 ---> 8e2ffcb73e11
41Step 2/7 : WORKDIR /go/src/app
42 ---> Running in 5b5a2dc4a338
43Removing intermediate container 5b5a2dc4a338
44 ---> 5b1fca84fda6
45Step 3/7 : ADD . /go/src/app
46 ---> d68aa9a0492c
47Step 4/7 : RUN go build -o /go/bin/app
48 ---> Running in fb397271454c
49Removing intermediate container fb397271454c
50 ---> c5dcdb595e45
51Step 5/7 : FROM gcr.io/distroless/base-debian10
52latest: Pulling from distroless/base-debian10
53be0788dcda67: Pulling fs layer
546c654ddf7049: Pulling fs layer
55be0788dcda67: Verifying Checksum
56be0788dcda67: Download complete
576c654ddf7049: Verifying Checksum
586c654ddf7049: Download complete
59be0788dcda67: Pull complete
606c654ddf7049: Pull complete
61Digest: sha256:50944f695c49bd0871c18e1298c283c9e54efa60a68f4ebba113a4d70b8a6c02
62Status: Downloaded newer image for gcr.io/distroless/base-debian10:latest
63 ---> 779982a7ef90
64Step 6/7 : COPY --from=build /go/bin/app /
65 ---> 0374ebb59b66
66Step 7/7 : CMD ["/app"]
67 ---> Running in 3a8173572d22
68Removing intermediate container 3a8173572d22
69 ---> 1b2930e04bf2
70Successfully built 1b2930e04bf2
71Successfully 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 します。

1version: 0.2
2
3env:
4  variables:
5    AWS_DEFAULT_REGION: ap-northeast-1
6    REPOSITORY_NAME: codebuild-docker-rate-limit-verification
7  parameter-store:
8    DOCKER_HUB_ID: /CodeBuild/DOCKER_HUB_ID
9    DOCKER_HUB_PASSWORD: /CodeBuild/DOCKER_HUB_PASSWORD
10    REPOSITORY_URI: /CodeBuild/REPOSITORY_URI
11
12phases:
13  pre_build:
14    commands:
15      - echo Logging in to Amazon ECR...
16      - aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | docker login --username AWS --password-stdin ${REPOSITORY_URI}
17      - echo Logging in to Docker Hub...
18      - echo ${DOCKER_HUB_PASSWORD} | docker login -u ${DOCKER_HUB_ID} --password-stdin
19+     - echo Pulling latest docker image...
20+     - docker pull ${REPOSITORY_URI}/${REPOSITORY_NAME}:latest
21  build:
22    commands:
23      - echo Docker build and push to Amazon ECR...
24-     - docker build -t ${REPOSITORY_NAME} .
25+     - docker build --cache-from ${REPOSITORY_URI}/${REPOSITORY_NAME}:latest -t ${REPOSITORY_NAME} .
26      - docker tag ${REPOSITORY_NAME}:latest ${REPOSITORY_URI}/${REPOSITORY_NAME}:latest
27      - docker push ${REPOSITORY_URI}/${REPOSITORY_NAME}:latest

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

1[Container] 2020/12/08 13:28:01 Running command docker build --cache-from ${REPOSITORY_URI}/${REPOSITORY_NAME}:latest -t ${REPOSITORY_NAME} .
2Sending build context to Docker daemon  22.53kB
3
4Step 1/7 : FROM golang:1.15.6-buster as build
51.15.6-buster: Pulling from library/golang
6756975cb9c7e: Pulling fs layer
7d77915b4e630: Pulling fs layer
85f37a0a41b6b: Pulling fs layer
996b2c1e36db5: Pulling fs layer
10145393847161: Pulling fs layer
113f8843661db9: Pulling fs layer
12218d240e42d4: Pulling fs layer
13145393847161: Waiting
143f8843661db9: Waiting
15218d240e42d4: Waiting
1696b2c1e36db5: Waiting
175f37a0a41b6b: Verifying Checksum
185f37a0a41b6b: Download complete
19d77915b4e630: Verifying Checksum
20d77915b4e630: Download complete
21756975cb9c7e: Download complete
2296b2c1e36db5: Verifying Checksum
2396b2c1e36db5: Download complete
24145393847161: Verifying Checksum
25145393847161: Download complete
26218d240e42d4: Verifying Checksum
27218d240e42d4: Download complete
283f8843661db9: Verifying Checksum
293f8843661db9: Download complete
30756975cb9c7e: Pull complete
31d77915b4e630: Pull complete
325f37a0a41b6b: Pull complete
3396b2c1e36db5: Pull complete
34145393847161: Pull complete
353f8843661db9: Pull complete
36218d240e42d4: Pull complete
37Digest: sha256:ac6f3a6f28356b4a00d0a9c93c7d637eb9b7fac48ce78700ae2a862fc096592b
38Status: Downloaded newer image for golang:1.15.6-buster
39 ---> 8e2ffcb73e11
40Step 2/7 : WORKDIR /go/src/app
41 ---> Running in a87e55523dc7
42Removing intermediate container a87e55523dc7
43 ---> a60cd7cc46ac
44Step 3/7 : ADD . /go/src/app
45 ---> de43f783cc81
46Step 4/7 : RUN go build -o /go/bin/app
47 ---> Running in aa848867d6fc
48Removing intermediate container aa848867d6fc
49 ---> ea449fa0369d
50Step 5/7 : FROM gcr.io/distroless/base-debian10
51latest: Pulling from distroless/base-debian10
52be0788dcda67: Already exists
536c654ddf7049: Already exists
54Digest: sha256:50944f695c49bd0871c18e1298c283c9e54efa60a68f4ebba113a4d70b8a6c02
55Status: Downloaded newer image for gcr.io/distroless/base-debian10:latest
56 ---> 779982a7ef90
57Step 6/7 : COPY --from=build /go/bin/app /
58 ---> Using cache
59 ---> b4e21827fc5c
60Step 7/7 : CMD ["/app"]
61 ---> Using cache
62 ---> 1b2930e04bf2
63Successfully built 1b2930e04bf2
64Successfully 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 で指定しているイメージを変更します。

1FROM golang:1.15.6-buster as build
2+ FROM public.ecr.aws/bitnami/golang:1.14.12-debian-10-r11 as build
3WORKDIR /go/src/app
4ADD . /go/src/app
5RUN go build -o /go/bin/app
6FROM gcr.io/distroless/base-debian10
7COPY --from=build /go/bin/app /
8CMD ["/app"]

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

今後の充実に期待ですね。

※追記

上記 rate limit の回避方法みたいな書き方をしていますが、実は Amazon ECR パブリックリポジトリにも制限があります。

2020/12/09現在 *英語* の料金ページに記載されています。(日本語ページにはのっていないので制限ないのかと)

https://aws.amazon.com/jp/ecr/pricing/?nc1=h_ls

Data Transfer OUT なので pull のデータ通信量だと思いますが

匿名ユーザだと 500 GB / Month

AWS アカウントだと 5 TB / Month

となっていました。

匿名ユーザの場合はやはりIPで制限されるのでしょうか。ちょっと記載は見つけれませんでした。

まとめ

当初の “Too Many Requests.” 対策としてはよっぽど高頻度のアクセス出ない限り Docker Hub にログインしておくで問題なさそう。

その他のキャッシュは高頻度でじっこうするプロジェクトでは効果あるはずですが、今回の検証では思った結果が得られませんでした。

そもそも build する docker イメージがもう少し凝ったものだと効果が出たかもしれませんね。

最後に効果が出ていた記事を参考にあげさせて今回の記事は以上といたします。

参考

https://dev.classmethod.jp/articles/codebuild-has-to-use-dockerhub-login-to-avoid-ip-gacha/https://tech.actindi.net/2019/07/06/132326