GitHub Self-hostedに移行しました。CIが最大55%速くなり、月額が300万円節約できた!
株式会社SODA / MINH DUC DANG
メンバー / SRE / 従業員規模: 301名〜500名 / エンジニア組織: 11名〜50名
利用プラン | 利用機能 | 事業形態 |
---|---|---|
Team Plan | テスト、デプロイ | B to C C to C |
利用プラン | Team Plan |
---|---|
利用機能 | テスト、デプロイ |
事業形態 | B to C C to C |
アーキテクチャ
アーキテクチャの意図・工夫
背景
毎月、AWSやGitHubなどのインフラストラクチャとツールの請求書をチェックしています。 GitHubの請求額が非常に高いことに気づきました。特にGitHub Actionsの部分です。
コストを削減するため、Self-hosted Runner(AWS EC2)を導入しました。
オールインワンソリューション
Googleで簡単に検索したところ、2つのリポジトリが目に留まりました。
- Runs-onプロジェクト: https://github.com/runs-on
- Philips Labs AWS self-hosted runnerプロジェクト: https://github.com/github-aws-runners/terraform-aws-github-runner どちらもオールインワンソリューションで、非常に良くドキュメント化されており、デプロイも簡単です。 私たちのIaCはTerraformなので、Phillips LabsのTerraformモジュールを選択しました。 注:Runs-onは商用目的で使用する場合、ライセンス料(年間$300)が必要です。
工夫したこと
NAT Gatewayの費用を削減する
数日後、請求書をチェックして、コストが非常に高いことに驚きました。
このプロジェクトに割り当てたタグでCost Explorerに日割りのコストを確認すると、90%はNAT Gatewayの費用でした。
当初、すべてのランナーインスタンスをプライベートサブネットに配置していたため、インターネットと通信する際にNAT Gatewayを使用する必要がありました。 ジョブを実行する前に以下の手順を行う必要があります。
- GitHubワークフローを実行するために必要なソフトウェアをインストールする(curl, git, dockerなど)
- ランナーはGitHub Action runner binaryを取得する
さらにジョブを実行するの際に必ず下記のステップが繰り返します。
- git checkout
- Dockerイメージを取得する
AWSはネットワークに入るデータには課金しませんが、NAT Gatewayを使用する場合、方向に関係なく、NAT Gatewayを通過するデータに対して課金します。 ランナーインスタンスの再利用時に発生する奇妙なエラーを避けるため、エフェメラルランナーを使用しました。これは、すべてのジョブが新しいインスタンスを取得し、完了時にインスタンスが廃止されます。また、上記のすべてのステップが繰り返し実行されるので、NAT Gatewayを通過する大量のネットワークトラフィックが発生しました。
インターネット通信を可能な限り最小限に抑えるため、アーキテクチャにいくつかの変更を行いました。
- 何もないベースイメージからランナーを作る代わりに、カスタムAMIをビルドして使用します。これにより、ランナーがジョブが実行できる状態までの時間が短縮されます。AMI内で、ワークフローで使用するすべてのソフトウェア(我々の場合はCGO用のCライブラリ、Golangマイグレーションツールなど)とDocker image(Golang、MySQL、Redisなど)を準備します。 (Phillips Labsのモジュールは、カスタムAMIをビルドするためのPackerテンプレートのサンプルも提供しています)
- Docker Hubからのイメージプル回数を減らし、彼らのレートリミットを避けるため、VPCエンドポイントでプルスルーキャッシュ設定のあるAWS Public ECRに切り替えました。
もう一つの選択肢は、プライベートサブネットの代わりにパブリックサブネットを使用することです。そうすると、各インスタンスに公開IPv4が付与され、独自にインターネットと通信できるため、NAT Gatewayは不要になります。しかし、これもまた、ランナーインスタンスがライフタイム中にインターネットに露出することを意味するため、最低限の通信のみの許可を設定しましょう。
結果として日額コストが$800から約$100に削減できました。
スポットインスタンスの停止率を抑える
コストを最適化するため、USリージョン(他のリージョンと比較して最も安価)でスポットインスタンスを活用しています。しかし、このスポットインスタンスタイプであるため、AWSが他の顧客のために必要とする場合はいつでも終了される可能性があります。
しかし、終了率を減らす手段があります。
デフォルトでは、EC2 Fleet APIはスポットインスタンスを取得の際にlowest price
戦略を使用します。
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-fleet-allocation-strategy.html
- Lowest price -> デフォルト
- Diversified
- Capacity optimized -> 最低終了率
- Price capacity optimized -> 最良の価格-終了率
終了率はAZ間で同じではありません。より多くの容量を余裕を持って持つAZでは、終了率が低くなるはずです。モジュールのinstance_allocation_strategy
パラメータで取得戦略を変更できます。
上記の画像からわかるように、
capacity optimized
戦略に切り替えた後、ランナーの終了率が大幅に改善されました。
(EC2での実行はGitHub-hostedよりも大幅に安価であるため、スポットでの少し高いコストは許容可能だと考えており、また、開発者がPRでジョブを再試行する必要が少なくなるため、より良い体験を提供します)
ジョブのキューイング時間を削減する
これには2つの理由がありました。
- 準備ステップ(Dockerインストール、Action binaryセットアップ)は、ランナーが起動する際に少なくとも1分かかる必要があります。これは上記で説明したカスタムAMIを使用することで解決できます。
- EC2のクォータ。
通常、本番ワークロードには東京リージョンを使用しているため、他のリージョンのクォータ制限にあまり気にしておりません。
最初の理由については、ランナーは1分程度でジョブを実行する準備が整うはずですが、なぜかそれよりジョブが開始までの時間が長くかかりました。
スケールアップlambda関数のログを調べたところ、EC2 Fleet APIを呼び出す際にいくつかのエラーが見つかりました。それは
MaxSpotInstanceCountExceeded
エラーでした。これは、AWSのクォータに達したためスポットリクエストが失敗したことを意味します。しかし、AWSコンソールでクォータをチェックしたところ、その時点まだ上限緩和申請はまだ指してなかったですが、制限はランダムな数字(648)でした。 スポットインスタンス制限クォータはのソフトリミットであることが判明しました。AWSは顧客の使用量を継続的に監視し、必要に応じて段階的に増加させます。しかし、私たちの場合、すべてのテストをself-hosted runnerを使用するように一気に切り替えたため使用量が突然増加し、このプロセスが追いつかず、制限エラーが発生しました。 妥当な制限をリクエストした後、キュー時間が大幅に短縮されました。

時々、複数の並列ジョブを持つワークフロー内で、その一部のみがqueued
状態に詰まります。GitHubコンソールは何も出力しないため、キューに入っているジョブがEC2に割り当てられているかどうか分かりませんでした。インスタンスIDがないため、どのインスタンスに問題があるかを特定できません。
色々調べた結果、これはAction Runner binaryのバグであることが判明しました。
https://github.com/actions/runner/issues/3609
何らかの理由で、ランナーインスタンスはジョブを拾いましたが、GitHubにログをプッシュしてステータスを報告することができず、永遠にqueued
状態に詰まります。このバグは現時点でもまだ解決されていないようです。
(AWSとAzureまたはGitHub間の接続問題に何らかの内部レート制限があると推測します)
進めないジョブについては、コンソールから手動でワークフローをキャンセルして再実行できます。しかし、ほぼ100近いのジョブをチェックして、本当に動かなかったのか、それとも実行順番を待っているだけなのかを確認するのは非常に面倒です。 最終的に、シンプルな対策を思いつきました。 GitHub-hosted runnerを使用して、15分ごとに実行されるスケジュールワークフローを実行し、15分以上キューに入っているジョブを含むワークフローがあるかどうかをチェックします。ある場合は、そのワークフローを強制的にキャンセルして再実行させます。
jobs:
retry-workflows:
runs-on: ubuntu-24.04
name: Retry Queued Workflows
steps:
- name: Check and retry queued workflows
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
QUEUED_RUNS=$(gh api --method GET /repos/{org}/{repository}/actions/runs -F status=queued --jq '.workflow_runs[] | .id')
CURRENT_TIME=$(date +%s)
for run_id in $QUEUED_RUNS; do
QUEUED_JOBS=$(gh api --method GET /repos/{org}/{repository}/actions/runs/"$run_id"/jobs --jq '.jobs[] | select(.status=="queued") | .id')
for job_id in $QUEUED_JOBS; do
# Get the created_at timestamp for the run
CREATED_AT=$(gh api --method GET /repos/{org}/{repository}/actions/jobs/"$job_id" --jq '.created_at')
CREATED_TIME=$(date -d "$CREATED_AT" +%s)
# Calculate how long the workflow has been queued (in minutes)
QUEUED_MINUTES=$(( ("$CURRENT_TIME" - "$CREATED_TIME") / 60 ))
echo "The job_id $job_id in the workflow $run_id has been queued for $QUEUED_MINUTES minutes"
# Only retry if queued time is between 15 and 120 minutes
if [ "$QUEUED_MINUTES" -ge 15 ] && [ "$QUEUED_MINUTES" -le 120 ] ; then
echo "Processing workflow run $run_id"
gh run cancel "$run_id"
sleep 5
for i in {1..5}; do
if gh run rerun "$run_id"; then
break
fi
echo "Retry $run_id attempt $i failed. Waiting 5 seconds before next attempt..."
sleep 5
done
break
fi
done
done
これにより、スタックしたジョブを手動でキャンセルして再試行する必要がなくなりました。
キャッシュバックエンドをGitHub cacheからS3に切り替え
GitHub Actionsには、ランタイムを短縮するためにジョブとワークフロー間でファイルをキャッシュバックエンドがあります。
しかし、どのプランでも各リポジトリは最大10GBのみです。私たちのリポジトリは少し複雑で、10GBでは不十分で、キャッシュファイルがGitHubによって頻繁に削除されていました。
EC2でself-hostedに移行したため、S3をキャッシュストレージとして使用する方が適切だと思います。利点としては
- 無制限の容量である
- S3 Gateway Endpointを使用するとスピードがすごく速い(実質300
400MB/sである。一方、GitHub Action Cacheは50100MB/sだけ) 上記で話したruns-onプロジェクトは、actions/cache
actionの代わりのactionがあります。
https://github.com/runs-on/cache
AWS_REGION
RUNS_ON_S3_BUCKET_CACHE
上記の2つの環境変数を追加し、EC2ランナーがS3バケットに適切にアクセスする権限を持っていることを確認を取れれば、利用できます。
他のパラメータはactions/cache
と同じで、S3を使用できない場合は自動的にGitHub Action Cacheにフォールバックしてくれます。
結果
itHub Action Performance Metricsの数値からみると、私たちのジョブは以前より平均 20% 高速になり、最良のシナリオでは最大 54% 速くなりました。
Test Workflow | Old (sec) | New (sec) | Change |
---|---|---|---|
Test Workflow 1 | 394.4 | 279.7 | 29.1% |
Test Workflow 2 | 311.3 | 233.1 | 25.1% |
Test Workflow 3 | 443.4 | 374.4 | 15% |
Test Workflow 4 | 495.3 | 548 | -10% |
※ Workflow中の各ジョブ平均実行時間で計算しました。
削減できたコストについては、毎月ワークロードが変わるので、移行前と移行後の請求書を比較するのはできないため、移行後実際利用量(分数)をGitHub Usage Metricsからを取って、GitHub-hostedを利用する場合、いくらかかるべきのか計算しました。
Machine Type | Total minutes | Price per min | Total Cost (USD) |
---|---|---|---|
linux-x64-2core | 4845 | 0.008 | 38.76 |
linux-x64-4core | 622319 | 0.016 | 9957.104 |
linux-x64-8core | 416835 | 0.032 | 13338.72 |
linux-arm64-2core | 145365 | 0.005 | 726.825 |
linux-arm64-4core | 1400 | 0.01 | 14 |
linux-arm64-8core | 116 | 0.02 | 2.32 |
Total cost in case of GitHub-Hosted | 24077.729 |
ご覧の通り使用量はGitHub-hostedで $24,000 かかるべきでしたが、self-hostedに移行した後ただ $3000 だけかかりました💸 これは 87.5% のコスト削減です!

導入の背景・解決したかった問題
導入背景
ツール導入前の課題
GitHub-hostedの場合、月額$24,000かかるべきでした。
どのような状態を目指していたか
AWS EC2 Spotを利用して、すべてのコストは$3,000だけでした。
導入の成果
改善したかった課題はどれくらい解決されたか
どのような成果が得られたか
導入に向けた社内への説明
上長・チームへの説明
無し
活用方法
よく使う機能
GitHub Actions self-hosted Runner
ツールの良い点
- 使いやすい
- コミュニティーが良い
ツールの課題点
- GitHub-hosted Runnerの値段が高い
ツールを検討されている方へ
Team PlanまたはGitHub Actions費用が高いの方はぜひself-hosted Runnerを検討してみてください。
株式会社SODA / MINH DUC DANG
メンバー / SRE / 従業員規模: 301名〜500名 / エンジニア組織: 11名〜50名
よく見られているレビュー
株式会社SODA / MINH DUC DANG
メンバー / SRE / 従業員規模: 301名〜500名 / エンジニア組織: 11名〜50名
レビューしているツール
目次
- アーキテクチャ
- 導入の背景・解決したかった問題
- 活用方法