🐳

AWS CodeBuildで DockerHubからのImageプルが「Download Rate Limit」エラーで失敗

2023/03/08に公開

こんにちわ。

DevelopersIO BASECAMP参加者の加藤です。

先日、はじめてしっかりとAWSのCodeシリーズを利用してのCI/CDと向き合う機会をいただき、見た事のないエラー文と遭遇しました!

その中のひとつが主題の「Download Rate Limit」エラーです。

toomanyrequests: You have reached your pull rate limit.
You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limit

内容・対処法、共に勉強になったので書き留めることにしました!

「Codeシリーズ」とは

CodeシリーズはAWSサービス中、CI/CD環境を構築するサービス群である「Code ○○」をまとめて表現したものです。

2023年6月現在、サービス名の先頭に「Code」とつく5つのサービスが存在するようです。

(AWS公式ハンズオンも存在していますのでご興味のある方は是非触ってみてください。)

当記事でもAWS公式ユーザーガイドから、それぞれのサービスの説明の簡単な所だけを抜粋してみました!

AWS Code Pipeline とは

ソフトウェアのリリースに必要なステップをモデル化、視覚化、自動化するために使用できる継続的な配信サービスです。

AWS CodeCommit とは

クラウド内のアセット(ドキュメント、ソースコード、バイナリファイルなど) を非公開で保存および管理するために使用できるアマゾン ウェブ サービスによってホストされるバージョン管理サービスです。

AWS Code Build とは

クラウドで動作する、完全マネージド型のビルドサービスです

AWS Code Deploy とは

Amazon EC2 インスタンス、オンプレミスインスタンス、サーバーレス Lambda 関数、または Amazon ECS サービスに対するアプリケーションのデプロイを自動化するデプロイサービスです。

AWS CodeStar とは

AWSでソフトウェア開発プロジェクトを作成、管理、および操作するクラウドベースのサービスです。

「コミット」、「ビルド」、「デプロイ」といった、単語のイメージがわからないとそれぞれの役割が想像しにくいかと思います。

かく言う私も完璧に理解したとは言うには遠いものの、
とてもざっくりとした説明にはなりますが、一連の流れをわかりやすいように次のように例えてみます。

Codeシリーズのそれぞれの役割

飲食店で、提供する料理を

・新開発
・改変

したい というシュチュエーションを例にします!


「CodeCommit」が
・「うちのお店でこんな料理作りたいよ!」
・「あのメニュー不評だから味付けとか盛り付け変えようぜ」
という諸々が書いてる紙が入っている箱!

「CodeBuild」が努力して完成品のメニューを作る人!

「CodeDeploy」が実際に調理してお客様に料理をお届けする人!

「CodePipeline」はそれを統制するまとめ役!

として流れにした図が以下です。


本当の所は微妙にニュアンスが違うかと思いますが、今回はざっくりとした理解の為の例えである為このような表現でお許しいただけたら幸いです。

(スターは今回の話に登場しない為割愛させてください)

詳しくはYoutubeのAmazon Web Services Japan 公式チャンネルのAWS Black Belt Online Seminer動画にCodeサービスを扱った動画があり、とてもわかりやすい説明なのでおすすめです。

冒頭のエラーの説明

先ほどの図を踏まえて、主題のエラーを簡単に表すとこんな感じになります。

このようにビルド段階でコケてしまったという状況が当該エラーになります。

(※"参考"というより「ベースになっている物あるから、もらってきてそれに色々やってね!」の方が近いかもしれません。)

DockerHubにおけるDownload Rate Limit(ダウンロード率制限)

Dockerドキュメントにはこう書かれております。

Docker Hub におけるダウンロード率制限とは

Docker Hub 上でプルリクエストを行うユーザーのアカウントタイプに応じて、Docker イメージのダウンロード (「pull」) にはいくつかの制限があります。 プル率の制限は個々の IP アドレスに基づきます。 匿名ユーザーの場合、プル率の制限は 1 つの IP アドレスにつき、6 時間ごとに 100 プルに設定されています。 認証されている ユーザーの場合は、6 時間ごとに 200 プルです。 有償の Docker サブスクリプションのユーザーには制限はありません。

この説明だけを読むと、「そんなに短時間の間に100回もプルしていないはずのになんでなんだろう?」となりそうなものですが、

察しのいい方は既にピンときている通り、ここで先ほどの引用文をもう一度見てみると、

プル率の制限は個々の IP アドレスに基づきます。

でした。

この「IP」はどのIPかというと、ローカルのIPではなく、あくまで「プルさせてね!」とアクションを起こしているのは「実行しているリージョンのCode BuildのIP」となります。

AWSにはリソースがどのIPを利用しているかを記載したip-ranges.jsonがありますが、ここで東京リージョン(ap-northeast-1)且つ、CODEBUILDであるIPを検索すると、2023/3/6時点では2つのIPがHITするようでした!

数年前のいくつかの記事では8個のIPとして紹介されていましたが、いずれにしてもDockerHubに匿名ユーザーとして認識されている場合は、数個のIP×100プルを利用ユーザーで共用している事になり、これがいわゆる「Buildに成功したり、失敗したり」というIPガチャ状態を引き起こしているとの事です。

対処方法① pullする前にDockerHubにログイン

早速対処方法ですが、色々な記事で紹介されていました。

”匿名ユーザー”でなくなってしまいましょう」という作戦になります!

先ほどの説明を整理すると

・匿名ユーザーは数個のIP(皆で共用)×100プルのガチャ。
・認証ユーザー(有償でないユーザー)はアカウント毎に200プル(自分のみ)。
・有償ユーザーは無制限。

となりますので、DockerHubに登録しているアカウントでログインをしてからプルアクションを取るようにします。

(※この記事に辿り着いてくださった方の大半がエラー文を検索して閲覧いただいている事が想定されるかと思いますので、buildspecの説明は割愛させていただきます!)


■ テンプレート

AWS::SecretsManager::Secret
AWS::IAM::Role
AWS::CodeBuild::Project

上記3リソースのみですので、このテンプレートだけで動くものではないですが、以下CloudFormationの例を書いてみました!

CloudFormationテンプレート
SampleTemplate.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Sample template for DockerHUB download rate limit error.
Parameters: 

  DockerHubUserName: #DockerHubのユーザー名
    Type: String
    NoEcho: true

  DockerHubPassword: #DockerHubのパスワード
    Type: String
    NoEcho: true
  
  PJPrefix: #リソース名の接頭辞に利用
    Type: String

Resources:
  DockerHubSecret:
    Type: AWS::SecretsManager::Secret
    Description: username and password for connection to DockerHub.
    Properties:
      Name: !Sub ${PJPrefix}-secret-for-docker-hub-connection
      SecretString: !Sub '{"username":"${DockerHubUserName}","password":"${DockerHubPassword}"}'

  SampleCodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${PJPrefix}-role-for-codebuild
      Path: /
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: CodeBuildAccess
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Resource: '*'
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
              - Effect: Allow
                Resource: !Sub arn:aws:s3:::《アーティファクトバケット名》/*
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketAcl
                  - s3:GetBucketLocation
              - Effect: Allow
                Action:
                  - codebuild:CreateReportGroup
                  - codebuild:CreateReport
                  - codebuild:UpdateReport
                  - codebuild:BatchPutTestCases
                  - codebuild:BatchPutCodeCoverages
                Resource: '*'
              - Effect: Allow
                Action:
                  - ecr:GetAuthorizationToken
                  - ecr:BatchCheckLayerAvailability
                  - ecr:GetDownloadUrlForLayer
                  - ecr:GetRepositoryPolicy
                  - ecr:DescribeRepositories
                  - ecr:ListImages
                  - ecr:DescribeImages
                  - ecr:BatchGetImage
                  - ecr:InitiateLayerUpload
                  - ecr:UploadLayerPart
                  - ecr:CompleteLayerUpload
                  - ecr:PutImage
                Resource: '*'
              - Effect: Allow
                Action:
                  - secretsmanager:GetResourcePolicy
                  - secretsmanager:GetSecretValue
                  - secretsmanager:DescribeSecret
                  - secretsmanager:ListSecretVersionIds
                Resource: '*'
              - Effect: Allow
                Action:
                  - ecs:DescribeTaskDefinition
                Resource: '*'

  SampleCodeBuildProject:
    Type: AWS::CodeBuild::Project
    DependsOn: DockerHubSecret
    Properties:
      ServiceRole: !Ref SampleCodeBuildServiceRole
      Artifacts:
        Type: CODEPIPELINE
      Source:
        Type: CODEPIPELINE
        BuildSpec: |
          version: 0.2
          env:
            variables:
              DOCKER_BUILDKIT: "1"
            secrets-manager:
              DOCKERHUB_USER: ${DockerHubSecretArn}:username
              DOCKERHUB_PASS: ${DockerHubSecretArn}:password
          phases:
            install:
              runtime-versions:
                docker: 20
            pre_build:
              commands:
                - AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
                - echo Logging in to ECR
                - aws ecr --region ${AWS_REGION} get-login-password | docker login --username AWS --password-stdin https://${REPOSITORY_URI}
                - echo Logging in to Docker Hub
                - echo $DOCKERHUB_PASS | docker login -u $DOCKERHUB_USER --password-stdin
                - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
                - IMAGE_TAG=${COMMIT_HASH:=latest}
            build:
              commands:
                - echo Build started on `date`
                - docker image build -t ${REPOSITORY_URI}:${COMMIT_HASH} .
            post_build:
              commands:
                - echo Build completed on `date`
                - echo Pushing the Docker images
                - docker image push ${REPOSITORY_URI}:${COMMIT_HASH}
                - IMAGE='<IMAGE_URI>'
                - aws ecs describe-task-definition --task-definition ${TaskDefinition} | jq '.taskDefinition | del (.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities)' | jq --arg image $IMAGE '.containerDefinitions[].image=$image' > taskdef.json
                - printf '{"Version":"1.0","ImageURI":"%s"}' $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
          artifacts:
              files:
              - taskdef.json
              - imageDetail.json
      Environment:
        PrivilegedMode: true
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/amazonlinux2-x86_64-standard:4.0
        Type: LINUX_CONTAINER
        EnvironmentVariables:
          - Name: AWS_REGION
            Value: !Ref AWS::Region
          - Name: REPOSITORY_URI
            Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/《作成済みのECR名》
          - Name: DockerHubSecretArn
            Value: !Ref DockerHubSecret
          - Name: TaskDefinition
            Value: 《作成済みのタスク定義名》
      Visibility: PRIVATE
      Name: !Sub ${PJPrefix}-build-project


Cloudformationコンソール画面でのパラメータ入力画面

■ 部分に分けた説明

詳細説明

① まず以下でDockerHubのユーザー名とパスワードの手入力を求めています。

Parameters: 
  DockerHubUserName:
    Type: String

  DockerHubPassword:
    Type: String

※DockerHubへブラウザ等からログインする際にメールアドレスとパスワードの組み合わせでログインしている場合でも、ここでの”DockerHubUserName”に登録メールアドレスを入力しても許容されず失敗しました。ご注意お願いします。


② 入力したそれぞれの文字列は以下のリソースでSecretsManagerにて
・ローテーションしない
・自動生成されたランダム文字列ではない
シークレットとして登録されます。

  DockerHubSecret:
    Type: AWS::SecretsManager::Secret
    Description: username and password for connection to DockerHub.
    Properties:
      Name: !Sub ${PJPrefix}-secret-for-docker-hub-connection
      SecretString: !Sub '{"username":"${DockerHubUserName}","password":"${DockerHubPassword}"}'

③ 以下の行で呼び出しているIAM RoleにはSecretsManagerに対する権限が付与されています。

      ServiceRole: !Ref SampleCodeBuildServiceRole
  SampleCodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
    #中略
              - Effect: Allow
                Action:
                  - secretsmanager:GetResourcePolicy
                  - secretsmanager:GetSecretValue
                  - secretsmanager:DescribeSecret
                  - secretsmanager:ListSecretVersionIds
                Resource: '*'

④ BuildSpec内の「env:」でシークレットマネージャーから呼び出して利用出来るようにします。

          env:
            variables:
              DOCKER_BUILDKIT: "1"
            secrets-manager:
              DOCKERHUB_USER: ${DockerHubSecretArn}:username
              DOCKERHUB_PASS: ${DockerHubSecretArn}:password

⑤ 「pre_build:」内の以下の部分で利用してログインします。

                - echo Logging in to Docker Hub
                - echo $DOCKERHUB_PASS | docker login -u $DOCKERHUB_USER --password-stdin

⑥ 無事先ほど失敗していたプルが問題なく成功している事が確認出来ました!

Buildlog
[Container] 2023/03/05 20:21:31 Running command echo Logging in to Docker Hub
Logging in to Docker Hub

[Container] 2023/03/05 20:21:31 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

※WARNING!部分についてはこちらの記事では「デフォルトの動作です」と記載がありますが、何らか暗号化処理が推奨されるものなのかもしれません。

対処方法② ECR PublicからのPull

正確にはECR Public Galleryからイメージをプルするという手段になります。

Amazon ECR パブリック ギャラリーは、Amazon ECR パブリック リポジトリでホストされているコンテナ イメージを見つけて共有するためのパブリック ウェブサイトです。パブリック リポジトリを参照してイメージをプルするために必要な認証はありません。https://gallery.ecr.awsの Amazon ECR パブリック ギャラリーにアクセスしてください。

以下の記事でも紹介されていますが、Dockerオフィシャルイメージが利用出来るようです。
「Docker オフィシャルイメージが ECR Public で利用可能になりました #reinvent」

認証も不要との事で、こちらにも存在するイメージの利用で問題はない場合は積極的に利用したいと思いました!


例:

Dockerfile
FROM public.ecr.aws/docker/library/httpd:latest

Buildlog
#3 [internal] load metadata for public.ecr.aws/docker/library/httpd:latest
#3 sha256:8551eb7037dd829c899ab4c31d76e58a1c9180f1fc54eab9c319356ce61ade5e
#3 DONE 1.4s
#中略
[Container] 2023/03/06 06:16:41 Phase complete: BUILD State: SUCCEEDED

まとめ

・AWS CodeBuildeにてDockerHubからイメージをpullする際にはダウンロード制限が存在する。
・CodeBuildはリージョン毎に数個のIPをユーザー全体で共有している。
・DockerHubはログインしていない場合、上記IPにてプルした回数をカウントしており、合計100回に達したIPには制限がかかりBuildは失敗する。
・制限を避ける為にはDockerHubにログインしてからプルするか、ECR Public Galleryからイメージをプルするようにする。

参考にしたブログ記事一覧

https://dev.classmethod.jp/articles/codebuild-has-to-use-dockerhub-login-to-avoid-ip-gacha/
https://zenn.dev/ttani/articles/aws-codebuild-docker-limit
https://tech.actindi.net/2022/01/11/081720
https://qiita.com/moyashidaisuke/items/de96e050b6fa0670a1df

終わりに

未知のエラーに遭遇する事で新しい知識が増えていくのが楽しいです!

お読みいただきありがとうございました!

デベキャン

Discussion