2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CircleCIからGitHub Actionsに移行したらECRの料金が跳ね上がった話

2
Posted at

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やクラウドをまたぐ通信が発生するときは、どこからどこへデータが流れて、どこに料金がかかるのかを意識していきたいです。

参考

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?