# GitHub Actions から OIDC で ECR Public にログインする — 429 Data limit exceeded の解消

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

- Source: https://oharu121.com/ja/blog/github-actions-ecr-public-oidc-429-data-limit-exceeded/
- Published: 2026-10-24T20:16:24+09:00
- Tags: AWS, GitHub Actions, GitHub, Docker

---
## はじめに

私が関わっているプロジェクトで、GitHub Actions のデプロイ用ワークフローが Dockerfile の 1 行目で失敗するようになりました。ベースイメージを取得する `FROM` の行です。

```text
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 exceeded
```

4 日間で 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 の料金ページ](https://aws.amazon.com/ecr/pricing/)によると、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` になります。

```yaml title=".github/workflows/deploy-dev.yml" ins={5-8}
      - 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 がプライベートレジストリにログインするときは別のホストのエントリを追加するので、このエントリには影響しません。

```json title="~/.docker/config.json（構造のみ）"
{
  "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` に固定しています。そのため、ワークフローにこのステップを追加するときに、リージョンを別途設定する必要はありません。

```js title="aws-actions/amazon-ecr-login/index.js"
  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` のままです。

*Figure — LoginFlow: ジョブの OIDC 認証情報で us-east-1 から 12 時間有効な ECR Public のトークンを取得し、ビルドはログイン済みのクライアントとして public.ecr.aws から取得します。*

### 必要な 2 つの権限と、組織の SCP の確認

ロールに必要なのは、レジストリのトークンを発行する `ecr-public:GetAuthorizationToken` と、ECR Public のレジストリ API が受け付けるベアラートークンを発行する `sts:GetServiceBearerToken` です。アクションの README に載っているのも、この 2 つだけです。

```json title="amazon-ecr-login の README にある最小ポリシー"
{
  "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 の判定です。

```bash
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	True
# sts:GetServiceBearerToken	allowed	True
```

## CDK のデフォルトのシンセサイザーで、どのロールに何の権限が要るか

**スタックのリソースを作るのは 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 | スタックが作成するすべてのリソースの権限 |

*Figure — RoleChain: GitHub のロールに必要なのは、ECR Public の 2 つのアクションのほかは、ブートストラップロールを引き受ける権限だけです。リソースは CloudFormation が cfn-exec-role の権限で作成するので、その権限は cfn-exec-role に持たせます。*

このアカウントでは両方のロールが `AdministratorAccess` を持っていたので、何も失敗しませんでした。この区別が効いてくるのは、GitHub 用のロールを絞り込むときです。ECS や Lambda の権限を残したまま **`sts:AssumeRole` を外すと `cdk deploy` が壊れます**。リソースの権限を持たせるべきなのは `cfn-exec-role` のほうで、デフォルトのブートストラップはこれを `AdministratorAccess` 付きで作成します。

## プルリクエストのビルドには、トークンの取得しかできないロールを割り当てる

ここまでは `main` へのデプロイで起きた 429 の話でしたが、プルリクエストごとにもイメージのビルドが実行されます。このビルドも同じベースイメージを匿名で取得していたので、**10 日間で 13 回、429 で失敗していました**。

ただし、デプロイ用ロールはこのビルドには流用できません。デプロイ用ロールの信頼ポリシーが受け入れるのは `main` での実行だけで、しかも `AdministratorAccess` を持っているため、プルリクエストを信頼させれば、どのブランチにもその権限を渡すことになります。

私はプルリクエスト用に、ECR Public の 2 つのアクションだけを許可するロールを別に作成しました。プルリクエストからトリガーされたワークフローがこのロールを引き受けても、できるのは公開イメージを取得するためのトークンの取得だけで、`AdministratorAccess` の権限は得られません。

*Figure — TwoRoles: デプロイ用ロールは引き続き main だけを信頼します。プルリクエストのビルドは、2 つのトークン用アクションだけを許可する、別に作成したロールを引き受けます。*

### 信頼ポリシーで pull_request イベントを指定する

GitHub の OIDC トークンには、リポジトリと実行のきっかけを表す `sub` クレームが入っています。`pull_request` イベントの場合、その末尾は `:pull_request` です。このリポジトリは GitHub のイミュータブルな subject 形式 `owner@owner-id/repo@repo-id` を使っています。2026 年 7 月 15 日より後に作られたリポジトリでは、この形式がデフォルトです。

```json title="取得専用ロールの信頼ポリシー" {10-13}
{
  "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 つのアクションだけが許可され、それ以外は拒否されることがわかります。

```bash
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	allowed
# sts:GetServiceBearerToken	allowed
# ecr:GetAuthorizationToken	implicitDeny
# s3:ListAllMyBuckets	implicitDeny
```

### Dependabot は ECR Public にログインせず、匿名で取得する

```yaml title=".github/workflows/ci-docker.yml"
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
```

1. **ロールの ARN はリポジトリのシークレットに置いています**。そのため、ワークフローのファイルにはアカウント ID が出てきません。組織のリポジトリでは、write 権限を持つユーザーならシークレットを作成できます。
2. **Dependabot の実行ではログインを飛ばします**。Dependabot がトリガーしたワークフローには GitHub が Actions のシークレットを渡さないため、`role-to-assume` が空になり、ステップが失敗します。Dependabot のプルリクエストは引き続き匿名で取得するので、429 に当たる可能性は残ります。
3. **ログインのステップは認証情報のステップの `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 の上限と取得レート）](https://docs.aws.amazon.com/AmazonECR/latest/public/public-service-quotas.html)
- [Amazon ECR の料金（認証済み取得の月 5 TB と、匿名取得の送信元 IP ごとの制限）](https://aws.amazon.com/ecr/pricing/)
- [Amazon ECR Public のエンドポイント（us-east-1 と us-west-2 のみ）](https://docs.aws.amazon.com/general/latest/gr/ecr-public.html)
- [Amazon ECR Public のレジストリ認証（us-east-1 で認証するよう記載）](https://docs.aws.amazon.com/AmazonECR/latest/public/public-registry-auth.html)
- [aws-actions/amazon-ecr-login](https://github.com/aws-actions/amazon-ecr-login)
- [GitHub の OIDC リファレンス（pull_request の subject とイミュータブルな subject 形式）](https://docs.github.com/en/actions/reference/security/oidc)
- [GitHub Actions でのシークレットの使用（作成できるユーザーと Dependabot）](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets)
