GiHub Actionsのコスト削減アイディア
こんにちは!
応援したい焼肉屋さんがあり、写真付きで口コミを書こう!と意気込んでいたのに、気がついたらパクパク頬張りまくってしまったこまっちゃんです。
さて、今回はGitHubのCI/CDツールである、GitHub Actionsのコスト削減について考えたいと思います!最初に前置きを記載しているため、削減方法のみ知りたいよ!という方は、目次より 「GitHub Actionsのコスト削減方法」まで飛んでください。
GitHub Actionsのコストはどこで発生するのか
そもそも、GitHub Actionsの料金はどうやって算出されるのでしょうか?
まず、最初にリポジトリの種類によって料金が変わります。
- パブリックリポジトリ
- 無料で利用できる(larger runnerの場合は有料)
- プライベートリポジトリ
- 従量課金制(使用時間 + ストレージ使用量)でコストが発生
- 端数(秒)は1分単位で切り上げ
- コストはGitHub Actionsのワークフローを実行するランナー(≒ 実行環境)の設定により変動
- 従量課金制(使用時間 + ストレージ使用量)でコストが発生
そこで、今後はプライベートリポジトリ、特にコストへのインパクトが大きい使用時間について掘り下げていきたいと思います。
さて、GitHub Actionsには、契約しているプランに応じて無料枠が適用できます。無料枠に収まる時間の利用であれば、プライベートリポジトリであっても無料で使用できる、というわけです(条件付き:後述)。
文字で見るのは複雑なので、一旦主なプラン別に料金を表にまとめてみましょう!
Free $0 | Team $48 | Enterprise $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つのランナーに分かれています。
- standard runner
- 無料枠適用対象
- OSは以下の3種類に限られる
- Linux 2コア
- Windows 2コア
- macOS 3 or 4コア(M1 or Intel)
- larger runner
- 無料枠適用対象外
- スタンダードランナーよりもスペックが高く、スタンダードランナーでは対象外であるOSやコア数もサポートしている
- コア数に比例して1分あたりの料金が倍になっている
- 静的IPアドレスを無料で割り当てられる
self-hosted runner とは?
ご自身で管理するカスタム環境でGitHub Actionsを実行するランナーです。GitHub Actionsの利用料金は発生しませんが、構築したインフラの料金が別途発生します。以下のようなケースで使用されることが多いです。
- GitHub-hosted runnerよりも細かくOSやVMを設定したい
- ランナーの静的IPアドレスを設定したい(larger runnerでも代用可)
GitHub Actionsのコスト削減方法
以上を踏まえると、コスト削減に向けた道のりは以下のようになりそうです。
- 実行したい環境や目的に応じて、ランナーの種類やスペックを最適化する
- フローを最適化し、実行時間(使用時間)を削減する
- 実行時間が長いワークフローを特定する
- 差分に応じてワークフローを実行する
- 重複するワークフローはキャンセルする
- ジョブを可能な限り直列処理にまとめる
- キャッシュを取得する
- (self-hostedの場合)ランナー実行環境のコスト最適化を実施する
本ブログでは、❷に注目してアイディアをまとめていこうと思います!
実行時間が長いワークフローの特定
コスト削減にあたり、まずは特にインパクトの大きいフローを特定します。GitHub Actionsをどの程度実行しているかについてはGitHub上で確認できます。特に時間のかかっているフローを特定後、さらにどのステップ/ジョブに時間がかかっているのか特定していくと良いです。
個人アカウントの場合
- GitHub右上のアバターアイコンをクリック
Settings
をクリックBilling and plans
を展開し、Plans and usage
をクリック- 下にスクロールして、
Usage this month
を確認する
組織やGitHub Enterpriseの場合
使用状況メトリクスから確認することができます。
※メトリクス表示機能が有効化されていて、かつ閲覧に必要な権限を付与されている場合
- 任意のリポジトリを開く
Insights
を選択Actions Usage Metrics
をクリック- ワークフロー別に実行時間が表示されるので、集計期間を目的の期間に変更しつつ使用状況を確認する
また、リポジトリのオーナーや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を使用しています。
重複するワークフローのキャンセル
例えば、コードをpushするたびにテストを実行するようにActionを設定していて、前のテストが終わる前に次のpushをするようなケースがこちらに該当します。GitHub Actionsは別個の環境で実行されるので、同じフローが重複して走る場合は、古いワークフローはキャンセルすると実行時間を短縮することができます。
キャンセルの手順ですが、各ワークフローファイルに以下のセクションを追加するだけです。同じグループ名のフローが始まると、進行中のフローがキャンセルされます。
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
ジョブを直列処理にまとめる
GitHub Actionsは、実行時間の端数(秒)を分に切り上げて請求が行われる仕組みなので、実行時間と請求時間はイコールではありません。
しかし、ここでポイントになるのが、ジョブ毎に分単位の切り上げが行われるということです。つまり、ジョブを細かく分けたり、並列処理をしたりするほど請求時間が増大することになります。
Job | Total duration | Billable time |
---|---|---|
1 | 10s | 1 min |
2 | 1min 55s | 2 min |
3 | 33s | 1 min |
合計 | 158s (< 3 min) | 4 min |
並列処理をすると高速にGitHub Actionsを実行することができますが、まとめられる処理は極力一つのジョブにまとめてしまい、実行時間と請求時間の差分を極力少なくするとコスト削減に繋がります。
キャッシュを取得
キャッシュの取得・リストアにかかる時間が発生するので必ずしもコスト削減につながるとは言い切れないのですが、一般的にキャッシュを取ると実行時間が短くなりやすいのでご紹介いたします。
キャッシュの取得には、GitHub公式のアクションを使用します。
キャッシュが存在するかどうかで後続のステップの処理を変更できるので、都度依存関係をインストールする必要がなくなります。キャッシュの取得・リストアは別ステップに分けられるので、最初のジョブでキャッシュを取得、後続ジョブでキャッシュをリストアする流れがよくみるかと思います。
キャッシュの取得
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構築を意識していきたいと思いました。
少しでも参考になる部分がありましたら幸いです!