用 OIDC 讓 GitHub Actions 登入 ECR Public — 解決 429 Data limit exceeded 的問題
GitHub Actions 的建置從 ECR Public 拉取映像時,出現了 429 Data limit exceeded 錯誤。使用該 job 既有的 OIDC 憑證登入 ECR Public 後,拉取就會計入 AWS 帳戶的配額,進而解決未登入時配額不足的問題。

本頁目錄
引言
在我參與的一個專案裡,GitHub Actions 的部署 workflow 在執行 Dockerfile 的第一行、也就是拉取基礎映像的 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 exceeded四天內失敗了四次,中間只成功過一次。這導致合併到 main 的變更一直沒有部署到 dev 環境。
這個 workflow 原本是在沒有登入的情況下,從 Amazon ECR Public 拉取基礎映像,因此遇到了配額不足的問題。用 job 既有的 OIDC 憑證登入 ECR Public 之後,問題就解決了。現在建置時的拉取會計入 AWS 帳戶自己的配額。
為什麼 GitHub Actions 的匿名拉取會配額不足?
ECR Public 給匿名拉取的配額是固定的,資料傳輸量和拉取速率這兩項上限都不能調高。Amazon ECR Public 是 AWS 的公開容器登錄服務,位於 public.ecr.aws,也提供 docker/library/python 這類 Docker 官方映像的鏡像。只要是沒有帶驗證 token 的拉取,都算是匿名拉取。
| 匿名 | 以 AWS 帳戶登入 | |
|---|---|---|
| 免費的網際網路資料傳輸量 | 每月 500 GB,不可調整 | 每月 5 TB |
| 拉取速率 | 每秒 1 次,不可調整 | 每秒 10 次,可調整 |
失敗的那幾次執行,有的印出 Data limit exceeded,有的只印出 429 Too Many Requests。如果兩項上限回傳的是同一個狀態碼,這兩種輸出就都說得通。
用掉那 500 GB 的並不是這個 workflow 本身。一次部署拉取的量遠低於 1 GB,而且從 9 月下旬到 10 月中旬的部署全都成功了。根據 ECR 的定價頁面,在沒有登入的情況下,能夠拉取的量是依來源 IP 限制的。GitHub 託管的 runner 和其他 GitHub 使用者共用對外 IP 位址,所以這些拉取和其他使用者用的很可能是同一份配額。
登入之後,配額改為依 AWS 帳戶計算,不再和同一個 IP 背後的其他使用者共用。每個帳戶每月 5 TB 以內的網際網路資料傳輸量都免費,而一次部署的拉取量不到 1 GB,完全在這個範圍內。
讓部署 job 登入 ECR Public
部署 job 只要在取得 AWS 憑證的步驟和建置之間新增一個步驟來登入就好。aws-actions/amazon-ecr-login 是 AWS 官方的 action,會把 job 的 AWS 憑證換成登錄服務的 token,再執行 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 主機的項目,所以這筆登入資訊適用於這個主機上的所有映像,docker/library/python 和 lambda/python 的基礎映像都包含在內。cdk deploy 會自己執行 docker build,這個建置讀的也是同一個檔案。CDK 登入私有登錄服務時,新增的是另一個主機的項目,不會動到這一筆。
{ "auths": { "public.ecr.aws": { "auth": "…" }, "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com": { "auth": "…" } }}為什麼 job 設定的區域是東京,登入時卻要用 us-east-1?
這個專案的其他部分都在 ap-northeast-1 執行,但是 ECR Public 的 API 只存在於 us-east-1 和 us-west-2。AWS 的登錄服務驗證頁面也寫著,驗證請求一律要送到 us-east-1。
這個 action 的原始碼裡已經把公開登錄服務的區域寫死成 us-east-1,所以在 workflow 裡加這個步驟時,不需要另外設定區域:
const ecrPublicClient = new ECRPUBLICClient({ // … // Authenticating to ECR Public auth only works in us-east-1 region: "us-east-1",映像仍然是從 public.ecr.aws 拉取,只有 token 的請求會送到 us-east-1。us-east-1 裡不會建立任何資源,之後的步驟也都維持在 ap-northeast-1。
需要的兩個權限,以及確認組織的 SCP
job assume 的 role 需要兩個權限:ecr-public:GetAuthorizationToken 用來取得登錄服務的 token,sts:GetServiceBearerToken 用來取得 ECR Public 登錄服務 API 接受的 bearer token。這個 action 的 README 列出的也正是這兩個:
{ "Effect": "Allow", "Action": [ "ecr-public:GetAuthorizationToken", "sts:GetServiceBearerToken" ], "Resource": "*"}service control policy(SCP)是 AWS Organizations 對組織內每個帳戶設下的限制。IAM policy 允許的動作,還要再通過 SCP 才會生效。即使 IAM 允許,SCP 還是可能拒絕對 us-east-1 的呼叫。最常見的例子,就是只允許核准區域的 SCP。
在修改 workflow 之前,我請 Claude Code 先確認有沒有這種 SCP。iam simulate-principal-policy 不用實際執行 job 就能回答這個問題,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 True在 CDK 預設的合成器下,哪個 role 需要哪些權限?
建立 stack 資源的不是 GitHub 的 role,而是 CloudFormation 的執行 role。沒有指定合成器(synthesizer)的 stack 會使用 CDK 的 DefaultStackSynthesizer,這時 cdk deploy 會 assume 名為 cdk-hnb659fds-* 的 bootstrap role。CloudFormation 則以 cdk-hnb659fds-cfn-exec-role-<account>-<region> 的身分建立所有資源。
| role | 由誰 assume | 需要的權限 |
|---|---|---|
| GitHub 的部署 role | GitHub Actions(透過 OIDC) | 對 cdk-hnb659fds-* 的 sts:AssumeRole,加上 ECR Public 的兩個動作 |
cdk-hnb659fds-cfn-exec-role-… | CloudFormation | stack 建立的每一項資源所需的權限 |
在這個 AWS 帳戶裡,兩個 role 都有 AdministratorAccess,所以權限怎麼分都不會出錯;但只要有人開始縮小 GitHub role 的權限,這個分工就很重要了。如果在 GitHub role 上保留 ECS 或 Lambda 的權限,卻拿掉 sts:AssumeRole,cdk deploy 就會失敗。資源的權限應該放在 cfn-exec-role 上,預設的 bootstrap 建立它時就已經附上 AdministratorAccess。
為 pull request 的建置準備一個只能取得 token 的 role
之前都在談 main 部署時遇到 429 的情況,但其實每個 pull request 也都會執行一次映像建置,這個建置之前也是用匿名方式拉取相同的基礎映像,所以十天內已經因為 429 失敗了 13 次。
但是部署 role 沒辦法直接拿來給 pull request 的建置使用,因為部署 role 的 trust policy 只接受 main 上的執行,本身又擁有 AdministratorAccess,要是讓它信任 pull request,任何分支都能拿到這份權限。
我為 pull request 另外建立了一個 role,權限只有 ECR Public 的那兩個動作。由 pull request 觸發的 workflow assume 這個 role 之後,能做的就只有取得拉取公開映像用的 token,而拿不到 AdministratorAccess 的權限。
在 trust policy 裡指定 pull_request 事件
GitHub 的 OIDC token 帶有一個 sub 宣告,內容是 repo 名稱和觸發這次執行的原因。如果是 pull_request 事件,sub 的結尾會是 :pull_request。這個 repo 使用 GitHub 不可變的 subject 格式 owner@owner-id/repo@repo-id,2026 年 7 月 15 日之後建立的 repo 預設都是這個格式:
{ "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 可以看到 repo 使用的是哪一種格式。對新的 role 執行 iam simulate-principal-policy,可以確認它允許那兩個動作,其他的則全部拒絕:
輸入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- role 的 ARN 放在 repo 的密鑰裡,所以 workflow 檔案裡不會出現帳戶 ID。在組織的 repo 裡,只要有寫入權限,任何使用者都能建立密鑰。
- Dependabot 的執行會跳過登入。由 Dependabot 觸發的 workflow 拿不到 GitHub Actions 的密鑰,如果不跳過,因為
role-to-assume會是空值所以步驟就會失敗。最終 Dependabot 的 pull request 仍然是匿名拉取,所以還是有可能會遇到 429。 - 登入步驟判斷的是取得憑證那個步驟的
outcome。Dependabot 的執行會跳過取得憑證的步驟,這時outcome是skipped,登入也會跟著跳過,這時建置就會和以前一樣匿名拉取。
總結
現在兩個 workflow 都會登入 ECR Public,拉取改為計入 AWS 帳戶每月 5 TB 的配額,不再和同一個 IP 的其他使用者共用匿名的 500 GB。
部署 job 沿用了原本的 role,只多加了兩個登入相關的動作。pull request 的建置則 assume 另外新建立的 role,這個 role 信任 pull_request 的 subject,而且只能取得拉取用的 token。
只要是從 public.ecr.aws 拉取基礎映像的 workflow,加上一個 amazon-ecr-login 步驟,再允許 ecr-public:GetAuthorizationToken 和 sts:GetServiceBearerToken 這兩個動作,就能解決配額不足的問題。同時請為 pull request 的建置準備專用的 role,不要放寬部署 role 的信任範圍。
參考連結
- 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




