GiHub Actionsのコスト削減アイディア

cost-savings
koma

こんにちは!
応援したい焼肉屋さんがあり、写真付きで口コミを書こう!と意気込んでいたのに、気がついたらパクパク頬張りまくってしまったこまっちゃんです。

さて、今回はGitHubのCI/CDツールである、GitHub Actionsのコスト削減について考えたいと思います!最初に前置きを記載しているため、削減方法のみ知りたいよ!という方は、目次より 「GitHub Actionsのコスト削減方法」まで飛んでください。

GitHub Actionsのコストはどこで発生するのか

そもそも、GitHub Actionsの料金はどうやって算出されるのでしょうか?
まず、最初にリポジトリの種類によって料金が変わります。

  • パブリックリポジトリ
    • 無料で利用できる(larger runnerの場合は有料)
  • プライベートリポジトリ
    • 従量課金制(使用時間 + ストレージ使用量)でコストが発生
      • 端数(秒)は1分単位で切り上げ
    • コストはGitHub Actionsのワークフローを実行するランナー(≒ 実行環境)の設定により変動

そこで、今後はプライベートリポジトリ、特にコストへのインパクトが大きい使用時間について掘り下げていきたいと思います。

さて、GitHub Actionsには、契約しているプランに応じて無料枠が適用できます。無料枠に収まる時間の利用であれば、プライベートリポジトリであっても無料で使用できる、というわけです(条件付き:後述)。

文字で見るのは複雑なので、一旦主なプラン別に料金を表にまとめてみましょう!

Free $0Team $48Enterprise $252
無料枠2000分/月3000分/月50000分/月
GitHub-hosted
runner
standard
無料枠適用OK
Linux$0.008/分
Windows$0.016/分(=Linux x2
macOS$0.08/分(=Linux x10
larger*
無料枠適用NG
*x64のみ抜粋
Linux$0.008〜0.256/分
Windows$0.032〜0.14/分
macOS$0.12/分
self-hosted
runner
-任意のOS・GitHub Actionsは無料
・ランナーマシンの維持費用が別途発生
※ブログ執筆時点の情報です。最新情報はこちら

ランナーの種類やOSによっても料金が異なっていますね。一番料金を安く抑えるには、standard runnerのLinux(2コア)を選択するとよさそうです。

ちなみに、GitHub-hosted larger runnerは料金に開きがありますが、これはコア数に応じて料金が変動するためです。さらに補足すると、アーキテクチャによっても料金は変動します。上記では分かりやすくするためにx64のみを抜粋して記載していますが、その他については表のキャプションに記載したリンクよりご確認ください。

さて、初めて見る単語も出てきたので、一旦ここで用語の整理をしてみましょう。

GitHub-hosted runner とは?

その名の通りGitHubがホストするランナーのことで、メンテナンスやアップグレードといったインフラの管理をする必要がないのが特徴です。ジョブ実行ごとにインスタンスが建てられるため、独立したCI/CDの実行が可能です。

一方、サポートしているOSや仮想マシン(VM)に限りがあるため、実行環境を細かくサポートしたい場合は self-hosted runner を利用されるのがおすすめです。

standard runner と larger runner の違い

GitHub-hosted runnerは大きく2つのランナーに分かれています。

  1. standard runner
    • 無料枠適用対象
    • OSは以下の3種類に限られる
      • Linux 2コア
      • Windows 2コア
      • macOS 3 or 4コア(M1 or Intel)
  2. larger runner
    • 無料枠適用対象外
    • スタンダードランナーよりもスペックが高く、スタンダードランナーでは対象外であるOSやコア数もサポートしている
    • コア数に比例して1分あたりの料金が倍になっている
    • 静的IPアドレスを無料で割り当てられる

self-hosted runner とは?

ご自身で管理するカスタム環境でGitHub Actionsを実行するランナーです。GitHub Actionsの利用料金は発生しませんが、構築したインフラの料金が別途発生します。以下のようなケースで使用されることが多いです。

  • GitHub-hosted runnerよりも細かくOSやVMを設定したい
  • ランナーの静的IPアドレスを設定したい(larger runnerでも代用可)

GitHub Actionsのコスト削減方法

以上を踏まえると、コスト削減に向けた道のりは以下のようになりそうです。

  1. 実行したい環境や目的に応じて、ランナーの種類やスペックを最適化する
  2. フローを最適化し、実行時間(使用時間)を削減する
    1. 実行時間が長いワークフローを特定する
    2. 差分に応じてワークフローを実行する
    3. 重複するワークフローはキャンセルする
    4. ジョブを可能な限り直列処理にまとめる
    5. キャッシュを取得する
  3. (self-hostedの場合)ランナー実行環境のコスト最適化を実施する

本ブログでは、❷に注目してアイディアをまとめていこうと思います!

実行時間が長いワークフローの特定

コスト削減にあたり、まずは特にインパクトの大きいフローを特定します。GitHub Actionsをどの程度実行しているかについてはGitHub上で確認できます。特に時間のかかっているフローを特定後、さらにどのステップ/ジョブに時間がかかっているのか特定していくと良いです。

個人アカウントの場合

  1. GitHub右上のアバターアイコンをクリック
  2. Settingsをクリック
  3. Billing and plansを展開し、Plans and usageをクリック
  4. 下にスクロールして、Usage this monthを確認する

組織やGitHub Enterpriseの場合

使用状況メトリクスから確認することができます。
※メトリクス表示機能が有効化されていて、かつ閲覧に必要な権限を付与されている場合

  1. 任意のリポジトリを開く
  2. Insights を選択
  3. Actions Usage Metrics をクリック
  4. ワークフロー別に実行時間が表示されるので、集計期間を目的の期間に変更しつつ使用状況を確認する

また、リポジトリのオーナーやBillingを閲覧する権限がある人は、Billingから使用状況を確認し、レポートを出力(またはメール送付)することもできます。その他、Organization全体の使用状況を確認したい場合など、詳細な手順は参考資料❷をご確認ください。

差分に応じたワークフローの実行

こちらは、Pull requestの差分に関わらずGitHub Actionsの全ステップを実行している方におすすめの手順です。「このファイルを変更したときだけ特定のジョブ/ステップを走らせたい!」という時に使うことができます。lintチェックや単体テスト、マルチスタック構成のIaCのplan(diff)実行といった、全ファイルに対して実行する必要がない処理の時短になります。

例では、以下の要件を満たすように実装しています。

  • VPCに関連するファイルの変更があった場合、vpcやdatabaseのスタックに対してplanを実行
  • cloudfrontに関連するファイルの変更があった場合、cloudfrontのスタックに対してのみplanを実行
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: dorny/paths-filter@v3
      id: filter
      with:
        filters: |
          vpc:
            - 'stacks/vpc-stack.ts'
          database:
            - 'stacks/database-stack.ts'
          cloudfront:
            - 'stacks/cloudfront-stack.ts'
    - if: steps.changes.outputs.vpc == 'true'
      name: terraform plan - vpc
      continue-on-error: true
      run: ‥‥
    - if: steps.changes.outputs.vpc == 'true' || steps.changes.outputs.database == 'true'
      name: terraform plan - database
      continue-on-error: true
      run: ‥‥
    - if: steps.changes.outputs.cloudfront == 'true'
      name: terraform plan - cloudfront
      continue-on-error: true
      run: ‥‥

どのファイルが変更されたかを検出して場合分けするために、以下のactionを使用しています。

dorny / paths-filter
dorny / paths-filter

重複するワークフローのキャンセル

例えば、コードをpushするたびにテストを実行するようにActionを設定していて、前のテストが終わる前に次のpushをするようなケースがこちらに該当します。GitHub Actionsは別個の環境で実行されるので、同じフローが重複して走る場合は、古いワークフローはキャンセルすると実行時間を短縮することができます。

キャンセルの手順ですが、各ワークフローファイルに以下のセクションを追加するだけです。同じグループ名のフローが始まると、進行中のフローがキャンセルされます。

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

グループ名を指定しない場合、全ての進行中のフローが停止されてしまうので注意してください

ジョブを直列処理にまとめる

この手順を実行すると、直列化することでかえって実行時間が伸びる可能性もあるため、バランスを見ながら実装してください

GitHub Actionsは、実行時間の端数(秒)を分に切り上げて請求が行われる仕組みなので、実行時間と請求時間はイコールではありません。

しかし、ここでポイントになるのが、ジョブ毎に分単位の切り上げが行われるということです。つまり、ジョブを細かく分けたり、並列処理をしたりするほど請求時間が増大することになります。

JobTotal durationBillable time
110s1 min
21min 55s2 min
333s1 min
合計158s (< 3 min)4 min
実行時間と請求時間の差異

並列処理をすると高速にGitHub Actionsを実行することができますが、まとめられる処理は極力一つのジョブにまとめてしまい、実行時間と請求時間の差分を極力少なくするとコスト削減に繋がります。

キャッシュを取得

キャッシュの取得・リストアにかかる時間が発生するので必ずしもコスト削減につながるとは言い切れないのですが、一般的にキャッシュを取ると実行時間が短くなりやすいのでご紹介いたします。

キャッシュの取得には、GitHub公式のアクションを使用します。

action / cache
action / cache

キャッシュが存在するかどうかで後続のステップの処理を変更できるので、都度依存関係をインストールする必要がなくなります。キャッシュの取得・リストアは別ステップに分けられるので、最初のジョブでキャッシュを取得、後続ジョブでキャッシュをリストアする流れがよくみるかと思います。

キャッシュの取得

steps:
  - uses: actions/checkout@v4
  - uses: actions/cache@v4
    id: cache
    with:
      path: path/to/dependencies
      key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
  - name: Install Dependencies
    if: steps.cache.outputs.cache-hit != 'true'
    run: /install.sh

キャッシュの復元

steps:
  - uses: actions/checkout@v4
  - uses: actions/cache/restore@v4
    id: cache
    with:
      path: path/to/dependencies
      key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
  - name: Install Dependencies
    if: steps.cache.outputs.cache-hit != 'true'
    run: /install.sh
  - name: Build
    run: /build.sh
  - name: Publish package to public
    run: /publish.sh

なお、nodeのバージョンによる動作不良を防ぐため、node_modules のキャッシュはnpmでは推奨されていません。どうしてもキャッシュする場合は、nodeのバージョンやOS、アーキテクチャなどをキーに含めて、動作不良の可能性を少しでも低らすと良いかもしれません。

しかし、そもそもnode_modules のキャッシュ取得・リストアにもある程度時間がかかるので、キャッシュを取らない選択肢も考慮に入れつつコスト削減を検討していきたいところです。

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    id: node
    with:
      node-version: 22
  - uses: actions/cache@v4
    id: cache-node
    with:
      path: **/node_modules
      key: ${{ runner.arch }}-${{ runner.os }}-node-${{ steps.node.outputs.node-version}}-${{ hashFiles('package-lock.json') }}
# ここに.npmのキャッシュを使うステップを入れてもいいかも
  - if: steps.cache-node.outputs.cache-hit != 'true'
    run: npm ci

さいごに

いかがでしたでしょうか。私自身GitHub Actionsはそこまで経験がなかったのですが、最近コスト削減に取り組む機会があったのでまとめてみました。

無料枠を使い切るまではなかなか意識が向きにくいところかと思いますが、いざ課金された時に慌てて対策を取るのも大変なので、日頃からスピードとコストを両立できるようなCI/CD構築を意識していきたいと思いました。
少しでも参考になる部分がありましたら幸いです!

AUTHOR
koma
koma
エンジニア
元々製薬業界で働いており、スクールを経てDWSに入社。主にgolangを使用したバックエンド業務に携わっているが、触ったことのない技術にも楽しく挑戦していきたいと考えている。趣味はスキューバダイビング。
記事URLをコピーしました