AWS CodeBuildで DockerHubからのImageプルが「Download Rate Limit」エラーで失敗
こんにちわ。
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テンプレート
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
⑥ 無事先ほど失敗していたプルが問題なく成功している事が確認出来ました!
[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」
認証も不要との事で、こちらにも存在するイメージの利用で問題はない場合は積極的に利用したいと思いました!
例:
FROM public.ecr.aws/docker/library/httpd:latest
↓
#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からイメージをプルするようにする。
参考にしたブログ記事一覧
終わりに
未知のエラーに遭遇する事で新しい知識が増えていくのが楽しいです!
お読みいただきありがとうございました!
Discussion