GitHub Actions から OIDC で ECR Public にログインする — 429 Data limit exceeded の解消
GitHub Actions のビルドが ECR Public からの取得で 429 Data limit exceeded エラーになりました。ジョブの既存の OIDC 認証情報で ECR Public にログインすると、取得が AWS アカウントの上限に計上され、匿名での上限不足が解消します。

目次
はじめに
私が関わっているプロジェクトで、GitHub Actions のデプロイ用ワークフローが Dockerfile の 1 行目で失敗するようになりました。ベースイメージを取得する FROM の行です。
ERROR: failed to copy: httpReadSeeker: failed open: unexpected status code https://public.ecr.aws/v2/docker/library/python/manifests/sha256:…: 429 Too Many Requests - Server message: toomanyrequests: Data limit exceeded4 日間で 4 回失敗し、成功したのはその間の 1 回だけでした。main にマージした変更が dev 環境に届かなくなっていました。
このワークフローは、ベースイメージを Amazon ECR Public からログインせずに取得していたため、匿名の上限に達していました。ジョブの既存の OIDC 認証情報で ECR Public にログインすることで解消しました。これでビルドの取得は AWS アカウント自身の上限に計上されるようになりました。
GitHub Actions からの匿名取得が上限に達する理由
ECR Public は匿名の取得に固定の枠を設けていて、データ転送量とレートのどちらの上限も引き上げられません。Amazon ECR Public は public.ecr.aws にある AWS の公開コンテナレジストリで、docker/library/python のような Docker 公式イメージのミラーも置かれています。認証トークンなしの取得は匿名として扱われます。
| 匿名 | AWS アカウントでログイン | |
|---|---|---|
| インターネットへの無料データ転送 | 月 500 GB、引き上げ不可 | 月 5 TB |
| 取得のレート | 毎秒 1 回、引き上げ不可 | 毎秒 10 回、引き上げ可 |
失敗した実行の中には Data limit exceeded を出したものと、429 Too Many Requests だけを出したものがありました。2 つの上限が同じステータスコードを返すと考えれば、どちらの出力も説明がつきます。
500 GB を使い切っていたのは、このワークフローではありません。1 回のデプロイで取得するのは 1 GB を大きく下回る量で、9 月下旬から 10 月中旬までのデプロイはすべて成功していました。ECR の料金ページによると、AWS アカウントを使わない場合のデータ転送は送信元 IP ごとに制限されます。GitHub ホストランナーは送信元アドレスをほかの GitHub 利用者と共有しているため、その利用者たちの取得も同じ枠から差し引かれていた可能性が高いです。
ログインすると、上限は AWS アカウントごとに計算されるようになり、同じ IP の背後にいるほかの利用者と共有しなくなります。インターネットへの転送は 1 アカウントあたり月 5 TB まで無料で、1 回のデプロイの取得量は 1 GB に満たないので、十分に収まります。
デプロイジョブを ECR Public にログインさせる
デプロイジョブは、AWS の認証情報とビルドの間にステップを 1 つ足すだけで直ります。aws-actions/amazon-ecr-login は AWS 公式のアクションで、ジョブの AWS 認証情報をレジストリのトークンに換えて docker login を実行します。registry-type: public を指定すると、ログイン先が public.ecr.aws になります。
- uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} aws-region: ap-northeast-1 - name: Login to Amazon ECR Public uses: aws-actions/amazon-ecr-login@v2 with: registry-type: public - name: deploy run: make deployログインすると、~/.docker/config.json に public.ecr.aws ホストのエントリが 1 つ書き込まれます。そのため、docker/library/python だけでなく lambda/python のベースイメージも含め、このホストのイメージすべてに効きます。cdk deploy は内部で docker build を実行し、そのビルドも同じファイルを読みます。CDK がプライベートレジストリにログインするときは別のホストのエントリを追加するので、このエントリには影響しません。
{ "auths": { "public.ecr.aws": { "auth": "…" }, "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com": { "auth": "…" } }}ログインだけ us-east-1 で、ジョブは東京のままでよい理由
このプロジェクトはほかのすべてを ap-northeast-1 で動かしていますが、ECR Public の API は us-east-1 と us-west-2 にしかありません。AWS のレジストリ認証のページにも、認証は常に us-east-1 に対して行うよう書かれています。
このアクションのソースコードは、パブリックレジストリのリージョンを us-east-1 に固定しています。そのため、ワークフローにこのステップを追加するときに、リージョンを別途設定する必要はありません。
const ecrPublicClient = new ECRPUBLICClient({ // … // Authenticating to ECR Public auth only works in us-east-1 region: "us-east-1",イメージの取得元は変わらず public.ecr.aws で、us-east-1 に行くのはトークンの要求だけです。us-east-1 には何も作られず、後続のステップはすべて ap-northeast-1 のままです。
必要な 2 つの権限と、組織の SCP の確認
ロールに必要なのは、レジストリのトークンを発行する ecr-public:GetAuthorizationToken と、ECR Public のレジストリ API が受け付けるベアラートークンを発行する sts:GetServiceBearerToken です。アクションの README に載っているのも、この 2 つだけです。
{ "Effect": "Allow", "Action": [ "ecr-public:GetAuthorizationToken", "sts:GetServiceBearerToken" ], "Resource": "*"}サービスコントロールポリシー(SCP)は、AWS Organizations が各アカウントの IAM ポリシーの上からかける制限です。IAM で許可されていても、us-east-1 への呼び出しを SCP が拒否することはありえます。よくあるのは、承認済みのリージョンだけを許可する SCP です。
私はワークフローに手を入れる前に、この呼び出しを拒否する SCP がないか Claude Code に確認させました。iam simulate-principal-policy を使えば、ジョブを実行せずに答えが出ます。AllowedByOrganizations の列が SCP の判定です。
入力aws iam simulate-principal-policy \ --policy-source-arn arn:aws:iam::123456789012:role/YOUR_DEPLOY_ROLE \ --action-names ecr-public:GetAuthorizationToken sts:GetServiceBearerToken \ --context-entries ContextKeyName=aws:RequestedRegion,ContextKeyValues=us-east-1,ContextKeyType=string \ --query 'EvaluationResults[].[EvalActionName,EvalDecision,OrganizationsDecisionDetail.AllowedByOrganizations]' \ --output text出力ecr-public:GetAuthorizationToken allowed Truests:GetServiceBearerToken allowed TrueCDK のデフォルトのシンセサイザーで、どのロールに何の権限が要るか
スタックのリソースを作るのは GitHub 用のロールではなく、CloudFormation の実行ロールです。シンセサイザーを指定していないスタックは CDK の DefaultStackSynthesizer を使い、cdk deploy は cdk-hnb659fds-* という名前のブートストラップロールを引き受けます。CloudFormation は、すべてのリソースを cdk-hnb659fds-cfn-exec-role-<account>-<region> の権限で作成します。
| ロール | 引き受けるもの | 必要な権限 |
|---|---|---|
| GitHub のデプロイ用ロール | GitHub Actions(OIDC 経由) | cdk-hnb659fds-* に対する sts:AssumeRole と、ECR Public の 2 つのアクション |
cdk-hnb659fds-cfn-exec-role-… | CloudFormation | スタックが作成するすべてのリソースの権限 |
このアカウントでは両方のロールが AdministratorAccess を持っていたので、何も失敗しませんでした。この区別が効いてくるのは、GitHub 用のロールを絞り込むときです。ECS や Lambda の権限を残したまま sts:AssumeRole を外すと cdk deploy が壊れます。リソースの権限を持たせるべきなのは cfn-exec-role のほうで、デフォルトのブートストラップはこれを AdministratorAccess 付きで作成します。
プルリクエストのビルドには、トークンの取得しかできないロールを割り当てる
ここまでは main へのデプロイで起きた 429 の話でしたが、プルリクエストごとにもイメージのビルドが実行されます。このビルドも同じベースイメージを匿名で取得していたので、10 日間で 13 回、429 で失敗していました。
ただし、デプロイ用ロールはこのビルドには流用できません。デプロイ用ロールの信頼ポリシーが受け入れるのは main での実行だけで、しかも AdministratorAccess を持っているため、プルリクエストを信頼させれば、どのブランチにもその権限を渡すことになります。
私はプルリクエスト用に、ECR Public の 2 つのアクションだけを許可するロールを別に作成しました。プルリクエストからトリガーされたワークフローがこのロールを引き受けても、できるのは公開イメージを取得するためのトークンの取得だけで、AdministratorAccess の権限は得られません。
信頼ポリシーで pull_request イベントを指定する
GitHub の OIDC トークンには、リポジトリと実行のきっかけを表す sub クレームが入っています。pull_request イベントの場合、その末尾は :pull_request です。このリポジトリは GitHub のイミュータブルな subject 形式 owner@owner-id/repo@repo-id を使っています。2026 年 7 月 15 日より後に作られたリポジトリでは、この形式がデフォルトです。
{ "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", "token.actions.githubusercontent.com:sub": [ "repo:YOUR_ORG@111111/YOUR_REPO@222222:pull_request", "repo:YOUR_ORG@111111/YOUR_REPO@222222:ref:refs/heads/main" ] } }}リポジトリがどちらの形式を使っているかは、gh api repos/<owner>/<repo>/actions/oidc/customization/sub で確認できます。新しいロールに対して iam simulate-principal-policy を実行すれば、2 つのアクションだけが許可され、それ以外は拒否されることがわかります。
入力aws iam simulate-principal-policy \ --policy-source-arn arn:aws:iam::123456789012:role/YOUR_PULL_ONLY_ROLE \ --action-names ecr-public:GetAuthorizationToken sts:GetServiceBearerToken ecr:GetAuthorizationToken s3:ListAllMyBuckets \ --query 'EvaluationResults[].[EvalActionName,EvalDecision]' --output text出力ecr-public:GetAuthorizationToken allowedsts:GetServiceBearerToken allowedecr:GetAuthorizationToken implicitDenys3:ListAllMyBuckets implicitDenyDependabot は ECR Public にログインせず、匿名で取得する
permissions: id-token: write contents: read
# … - name: AWS credentials for the ECR Public login id: aws if: github.actor != 'dependabot[bot]' continue-on-error: true uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: ${{ secrets.AWS_ECR_PUBLIC_ROLE_ARN }} aws-region: us-east-1 - name: Login to Amazon ECR Public if: steps.aws.outcome == 'success' continue-on-error: true uses: aws-actions/amazon-ecr-login@v2 with: registry-type: public- ロールの ARN はリポジトリのシークレットに置いています。そのため、ワークフローのファイルにはアカウント ID が出てきません。組織のリポジトリでは、write 権限を持つユーザーならシークレットを作成できます。
- Dependabot の実行ではログインを飛ばします。Dependabot がトリガーしたワークフローには GitHub が Actions のシークレットを渡さないため、
role-to-assumeが空になり、ステップが失敗します。Dependabot のプルリクエストは引き続き匿名で取得するので、429 に当たる可能性は残ります。 - ログインのステップは認証情報のステップの
outcomeを見て判定します。Dependabot の実行では認証情報のステップがスキップされてoutcomeがskippedになるので、ログインもスキップされ、ビルドは以前と同じく匿名で取得します。
まとめ
どちらのワークフローも ECR Public にログインするようになりました。そのため、取得は同じ IP のほかの利用者と共有する匿名の 500 GB ではなく、AWS アカウントの月 5 TB に計上されます。
デプロイジョブは既存のロールに IAM のアクションを 2 つ足して使い回しています。プルリクエストのビルドは別のロールを引き受けます。このロールは pull_request の subject を信頼し、取得用トークンの発行しかできません。
public.ecr.aws からビルドするワークフローなら、amazon-ecr-login のステップを 1 つと、ecr-public:GetAuthorizationToken と sts:GetServiceBearerToken の 2 つのアクションで同じ結果になります。プルリクエストのビルドには、デプロイ用ロールの信頼を広げるのではなく専用のロールを用意してください。
参考リンク
- Amazon ECR Public のサービスクォータ(匿名の 500 GB の上限と取得レート)
- Amazon ECR の料金(認証済み取得の月 5 TB と、匿名取得の送信元 IP ごとの制限)
- Amazon ECR Public のエンドポイント(us-east-1 と us-west-2 のみ)
- Amazon ECR Public のレジストリ認証(us-east-1 で認証するよう記載)
- aws-actions/amazon-ecr-login
- GitHub の OIDC リファレンス(pull_request の subject とイミュータブルな subject 形式)
- GitHub Actions でのシークレットの使用(作成できるユーザーと Dependabot)




