TL;DR
- CIをCircleCIからGitHub Actions(以下GHA)に移行し、テストジョブを本番と共通のベースイメージで動かすようにしたところ、ECRのデータ転送料金がだいたい5倍に増えた
- 原因は、AWSの外にあるGHAのランナーからのpullが増えて、インターネットへの転送量がだいたい5倍になったこと
- テスト用イメージをGHCRに置くことで、データ転送料金を増加時の約1/20まで減らし、ワークフローも簡素化できた
背景:CircleCIからGHAへ移行する
今仕事で関わっている環境ではCIをCircleCIで実行していたのですが、しばしばジョブのキュー待ちが長くなったり(長いときは10分くらいまで)、PRやissueとの連携がしづらかったりして、使い続ける理由がなくなっていたのでGHAへの移行を決めました。
今回取り上げるのは、LintやRSpecなどのテストジョブを移行したときにハマった問題です。
発端:「本番と同じイメージでテストしたい」
GHAへの移行当初は、ランナー上で依存パッケージをインストールして、テスト環境を構築していました。
しかし、この方法には次の課題がありました。
- 本番環境のコンテナイメージとテスト環境のコンテナイメージが完全一致しないのでテストの品質に疑問の声が上がった
- Dockerfileの変更に合わせて、CI側の環境構築も変更する必要がある
- 環境構築の設定が複数箇所に分散する
そこで、本番環境と共通のDockerfileから作成したベースイメージを、テストジョブのコンテナとして使うことにしました。
旧CIでもECRに保存した同じベースイメージを使っていたので、新しいCIでも同じ構成を採用しました。
以下は構成を説明するために簡略化した例です。
jobs:
prepare:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
outputs:
image: ${{ steps.image.outputs.tag }}
registry_password: ${{ steps.registry.outputs.password }}
steps:
# AWS認証、イメージタグとECRのパスワードの取得を行う
# ...
test:
needs: [prepare]
runs-on: ubuntu-latest
container:
image: ${{ needs.prepare.outputs.image }}
credentials:
username: AWS
password: ${{ needs.prepare.outputs.registry_password }}
steps:
# テストを実行する
# ...
この時点でテスト自体は正常に動いていて、移行は問題なく完了したように見えました。
何が起きたか:ECRの料金が急増
変更をマージした後にAWSのコストレポートを確認すると、ECRのデータ転送料金がだいたい5倍に増えていました。
AWS Cost Explorerで、サービスをECRに絞り、使用タイプごとに日次の料金を確認すると、増加の要因はストレージではなく、インターネットへのデータ転送を示す APN1-DataTransfer-Out-Bytes でした。
原因:AWSの外へのpullが増えた
ECRからのpullは東京リージョンの外に出ると有料
今回の件で初めて知ったのですが、当時のGitHubホストランナーのうち、LinuxとWindowsのランナーはAzure上で動いていました。そのためECRからのpullは、AWSからインターネットへのデータ転送になっていました。
2026年10月28日に、条件を満たすLinuxランナーはAWS上でも動く場合があると発表されました。ランナーが動くクラウドは変わることがあるので、最新の情報は公式ドキュメントを確認してください。
ECRの料金は、転送の方向と転送先によって次のように変わるようです。
| 転送 | 料金 |
|---|---|
| ECRへの転送(push) | 無料 |
| 同一リージョンのAWSサービスへの転送 | 無料 |
| 別リージョンのAWSサービスへの転送(pull) | 有料(東京からus-east-1へは$0.09/GB) |
| インターネットへの転送(pull) | 有料(東京リージョンは$0.114/GB。最初の10TB/月、無料枠の超過分) |
一方、CircleCIのジョブは米国のAWSやGCPのリージョンで動いています。ECRは東京リージョンにあるので、CircleCIからのpullにもデータ転送料金がかかっていました。
つまり、AWSの外に出たことで単価が上がったわけではありません。APN1-DataTransfer-Out-Bytes の単価はマージの前後で同じなので、料金が約5倍なら、転送量も約5倍です。
Before:移行前(CircleCI)
After:移行後(当時のGitHub Actions)
イメージの取得量が積み重なる
テストジョブでは、アプリケーションのベースイメージに加えて、テストに必要な関連イメージも取得します。
概算の転送量は、次のように考えられます。
1回のCI実行における転送量
≒ 各イメージの取得量の合計
× イメージを取得するジョブ数
並列実行するジョブが多いと、同じイメージを複数のジョブが取得するので、転送量が増えます。実際の転送量は、レイヤーのキャッシュなどにも左右されます。
GHAだから必ず高くなるわけではない
重要なのは、GHAを使っているかどうかだけではありません。
たとえばAWS上のself-hosted runnerを使う構成なら、ネットワーク構成によっては、ECRとの通信にかかる料金を抑えられます。
CIの料金を考えるときは、CIサービスそのものだけでなく、ランナーがどこで動き、どのサービスと通信しているかを確認する必要があるということですね。
対策:GHCRからpullするようにした
テスト用のベースイメージをGitHub Container Registry(GHCR)に置き、そこからpullするようにしました。
GHCRを選んだ理由は、GitHubの提供範囲で完結して、ECRからのデータ転送が発生しなくなるからです。また、GHCRのストレージと帯域は執筆時点で無料です。ただし、GitHubの公式ドキュメントには「現在は無料」と書かれていて、変更する場合は1か月以上前に通知するとされているので、今後変わる可能性はあります。
対策後(GitHub Actions + GHCR)
他には、次のような選択肢もあります。
| 対策 | メリット | 注意点 |
|---|---|---|
| AWS上にセルフホストランナーを構築する | AWS内の通信経路を使える可能性がある | ランナーの運用・保守コストが発生する |
| コンテナイメージを軽量化する | 転送量を減らせる | 転送料金がかかる構造は残る |
| CI内で依存パッケージをインストールする | レジストリへの依存を減らせる | 環境設定の二重管理につながる |
実装
1. ベースイメージをGHCRにpushする
従来のイメージビルド処理を再利用して、GHCRにイメージをpushするワークフローを追加します。
以下は実装の考え方を示した例です。
name: Build and Push Test Image
on:
push:
branches:
- main
permissions:
contents: read
packages: write
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Log in to GHCR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ACTOR: ${{ github.actor }}
run: |
echo "${GITHUB_TOKEN}" \
| docker login ghcr.io \
-u "${ACTOR}" \
--password-stdin
- name: Build images
run: ./scripts/build-images.sh
- name: Push image
env:
IMAGE: ghcr.io/example/test-image
TAG: latest
run: |
docker tag local-test-image "${IMAGE}:${TAG}"
docker push "${IMAGE}:${TAG}"
実際の運用では、固定の latest タグだけに頼らず、使うイメージのバージョンを管理することも重要です。古いイメージを定期的に削除する仕組みも検討します。
2. テストジョブの取得先を変更する
テストジョブでは、イメージの取得先と認証方法を変更します。
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
container:
image: ghcr.io/example/test-image:latest
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Run tests
run: ./scripts/run-tests.sh
この構成では、GHAの GITHUB_TOKEN を使ってGHCRに認証します。プライベートイメージを使う場合は、パッケージのアクセス権やリポジトリとの関連付けも確認してください。
この変更で、AWSの認証、ECRへのログイン、認証情報の受け渡しといった処理が不要になりました。
※ 上記は説明用のサンプルです。実際のワークフローに適用する際は、ジョブの実行条件、権限、イメージタグ、テストの実行方法などを環境に合わせて調整してください。
結果:データ転送料金を大幅に削減
変更後にコストレポートを確認すると、ECRのデータ転送料金は大幅に減り、主にストレージの料金が残る形になりました。データ転送料金は、増加時の約1/20、ベースイメージへ切り替える前と比べても約1/4まで下がりました。
終わりに
GHAやCircleCIなどのSaaSのランナーが、どのクラウド環境で動いているのかを意識していなかったので、落とし穴にハマってしまいました。。
今後、CI/CDでSaaSやクラウドをまたぐ通信が発生するときは、どこからどこへデータが流れて、どこに料金がかかるのかを意識していきたいです。
参考
- Amazon ECR の料金
- GitHub-hosted runners(GitHub Docs)
- GitHub Actions adds multi-cloud compute to improve reliability(GitHub Changelog)
- Restrict your pipeline traffic to specific IP ranges(CircleCI Docs)
- Best Practices for Minimizing AWS Data Transfer Costs with CircleCI and ECR(CircleCI Support)
- How use of GitHub Packages is measured(GitHub Docs)