🎯

Self-Hosted Runnerで GitHub Actions のCI/CDを高速化・コスト最適化した

に公開

はじめに

Dress Code Advent Calendar 2025 の10日目の記事です。

GitHub Actionsは非常に便利なCI/CDツールですが、プロジェクトが成長するにつれて、GitHub-hosted runnersだけでは対応しきれない課題に直面することがあります。

本記事では、AWS上にSelf-Hosted Runnerを構築し、CI/CDパイプラインの高速化とコスト最適化を実現した取り組みについて紹介します。同様の課題を抱えている方の参考になれば幸いです。

背景・課題

私たちのプロジェクトでは、GitHub-hosted runnersを使用する中でいくつかの課題が顕在化していました。

リソース不足

プロジェクトの規模が大きくなるにつれ、ビルドやテストに必要なCPUとメモリが増加しました。特にNode.jsアプリケーションの大規模なビルドでは、GitHub-hosted runnersのスペックでは実行時間が長くなりがちでした。より高いリソースを持つ環境で実行することで、ビルド時間を短縮したいという要望がありました。

並列数上限によるキュー待ち

PRの数が増えると、GitHub-hosted runnersの並列実行数上限に達し、特にリリース前にはジョブが待機状態になることが頻繁に発生していました。開発者の生産性を維持するためにも、この待ち時間を解消する必要がありました。

デプロイ数の増加によるコスト増加

GitHub Actionsの課金は実行時間に基づくため、ワークフローの実行回数が増えるほど、毎月のコストも右肩上がりで増加していきます。月にActionの予算を決めて運用していましたが、月末になると予算を使い切りちょっとずつクレジットを増やしていくような運用になっていました。

これらの課題を総合的に解決するため、AWS上にSelf-Hosted Runnerを構築することを決定しました。

選ぶにあたって重視したこと

Self-Hosted Runnerの構築方法を検討する際、私たちが特に重視したポイントを整理します。

シングルスレッド性能とメモリ

TypeScriptのビルド(tscによるトランスパイル)や、Jest/Vitestによるテスト実行は、シングルスレッド性能に大きく依存します。並列実行できる部分もありますが、個々のファイルのコンパイルやテストケースの実行はシングルスレッドで処理されるため、コア数を増やすよりもシングルスレッド性能の高いCPUを選ぶ方が効果的です。

また、TypeScriptのビルドは型チェックのためにプロジェクト全体の型情報をメモリに保持する必要があり、大規模なプロジェクトでは数GBのメモリを消費することも珍しくありません。メモリ不足でビルドが失敗したり、スワップが発生して極端に遅くなったりする事態は避けたいところです。

Warm Pool機能

今回の一番の目的が実行時間の削減だったため、インスタンスが実行開始されるまでの待ち時間も最小限にする必要がありました。

理想は、ジョブがキューに入った瞬間に即座に実行が開始されること。そのためには、事前に起動済みのインスタンスをプールしておくWarm Pool機能が必要でした。インスタンスの起動を待つ時間をゼロにすることで、開発者はPRをプッシュした直後からビルド・テストの進捗を確認できます。

コスト意識

実行時間削減が目的ではありますが、EC2のオンデマンドインスタンスを常時起動しておくような運用は避けたいところでした。
スタートアップゆえ、技術選定におけるコスト意識も大事にしたいところです。

技術選定

terraform-aws-github-runnerの選定

Self-Hosted Runnerを構築するにあたり、terraform-aws-github-runnerを採用しました。

このTerraformモジュールは、AWS上でスケーラブルなSelf-Hosted Runnerを構築するためのモジュールです。GitHubのWebhookイベントをトリガーにして、必要なときだけEC2インスタンスを起動し、ジョブ完了後に自動的に終了するという仕組みを、低い実装コストで実現できそうなため採用しました。

このモジュールを選んだ決め手となった特徴を詳しく説明します。

スケーラブルなアーキテクチャ

terraform-aws-github-runnerは、AWS LambdaとEC2を組み合わせたイベント駆動型のアーキテクチャを採用しています。

GitHubでワークフローがトリガーされると、Webhookイベントが発火し、API Gateway経由でLambda関数が呼び出されます。このLambda関数がEC2インスタンスを起動し、GitHub Actionsのジョブを実行します。ジョブが完了すると、インスタンスは自動的に終了します。

この仕組みにより、ジョブの需要に応じて自動的にスケールアップ・ダウンが可能です。これにより、リリース前にPRが集中してもCI/CDの実行が滞留することはなくなりました。

コスト最適化

terraform-aws-github-runnerは、Spot Instanceを活用することができ、コストも抑えることができました。

もちろん、Spot Instanceには中断のリスクがありますが、CI/CDジョブは基本的に冪等(何度実行しても同じ結果になる)であり、中断されても再実行すれば問題ありません。私たちはcapacity-optimized戦略を採用し、中断リスクの低いインスタンスタイプを優先的に選択することで、実際の中断発生を最小限に抑えています。

Warm Pool機能による待機時間ゼロ

Spot Instanceを使用する場合、インスタンスの起動に平均2分程度かかるため、待機時間を解消するためにWarm Poolを有効化しています。

Warm Poolとは、事前に起動済みのインスタンスをプールしておき、ジョブがキューに入った瞬間に即座に割り当てる仕組みです。これにより、ジョブの待機時間を実質ゼロにすることができます。

後述しますが、私たちは平日の稼働時間帯のみWarm Poolを維持するスケジュールを設定し、コストと待機時間のバランスを取っています。

セキュリティの確保

セキュリティ面では、Ephemeral Runners(一時的なRunner)を採用しています。これは、1つのジョブを実行した後、インスタンスを破棄する方式です。

従来の永続的なSelf-Hosted Runnerでは、前のジョブで生成されたファイルやキャッシュが残り、セキュリティリスクやジョブ間の干渉が発生する可能性がありました。Ephemeral Runnersでは、毎回クリーンな環境でジョブが実行されるため、これらの問題を根本的に解消できます。

また、インスタンスへのアクセスはSSM Session Managerを使用しており、SSH鍵を管理する必要がありません。セキュリティグループでSSHポートを開放する必要もなく、よりセキュアな運用が可能です。

アーキテクチャ概要

全体のアーキテクチャを図示すると以下のようになります。

GitHub上でワークフローがトリガーされると、Webhookを通じてAWS側のAPI Gatewayにリクエストが送られます。これを受けてLambda関数が起動し、Warm Poolから利用可能なインスタンスを割り当てるか、新しいSpot Instanceを起動します。ジョブが完了すると、インスタンスは自動的に終了し、次のジョブに備えます。

カスタマイズした構成

terraform-aws-github-runnerをベースに、私たちのプロジェクトに最適化したカスタマイズを行いました。

1. ARM64アーキテクチャの採用

インスタンスタイプとして、AWS Graviton4プロセッサを搭載したr8gd.2xlargeを選択しました。

Graviton4は、AWSが独自に設計したARMベースのプロセッサで、性能とコストのバランスが良く、Spot Instanceと組み合わせることで、さらにコストを抑えることができています。

r8gd.2xlargeは8vCPUと64GBのメモリを搭載しており、Node.jsアプリケーションの大規模なビルドやテストでも十分なリソースを確保できます。当初はr8g.2xlargeを検討していましたが、Spot市場での中断率が高めだったため、r8gd.2xlargeに変更しました。インスタンスタイプの選択ではスペックだけでなく、リージョンに応じた中断率も考慮する必要があります。

スポットインスタンスアドバイザー | AWS

2. カスタムAMIの構築

ジョブ開始時のセットアップ時間を最小化するため、Packerを使用してカスタムAMIを構築しました。CI/CDの実行に必要なソフトウェアをプリインストールしています。
Packerを使用することで、これらのソフトウェアもコード管理が可能です。

  • 複数バージョンのNode.js
  • Docker Engine
  • Docker Compose v2

3. Warm Pool設定

前述のとおり、Warm Pool機能を活用してジョブの待機時間を最小化しています。ただし、24時間365日インスタンスを起動しておくのはコストが無駄に発生してしまうため、業務時間に合わせたスケジュールを設定しました。この運用により、業務時間中は待機時間ゼロの快適な開発体験を提供しつつ、夜間・休日のコストを抑制するバランスを実現しています。

pool_config = [
  {
    # 平日の8-20時までにwarm poolを4台に増やす
    schedule_expression          = "cron(* 8-20 ? * MON-FRI *)"
    schedule_expression_timezone = "Asia/Tokyo"
    size                         = 4
  },
]

コスト試算

最後に、この構成でのコストを試算してみましょう。

前提条件

東京リージョン(ap-northeast-1)でのr8gd.2xlargeのSpot価格は、記事執筆時点で約$0.2317/時間です。(Spot価格は需給により変動します)

Warm Pool運用時のコスト

私たちの設定では、平日の8:00〜20:00(12時間)に4台のインスタンスを起動しています。月の営業日を22日として計算すると:

4台 × 12時間/日 × 22営業日 = 1,056時間/月
1,056時間 × $0.2317/時間 = 約$244/月

これに加えて、Warm Pool外の時間帯にオンデマンドで起動されるインスタンスのコストがあります。実際の使用状況によりますが、全体で月額$250〜400程度になり、Github Actionsより少し多いくらいに収まっています。インスタンスの起動時間(2分)程度が許容できるのであれば、Warm Poolを削減することで、かなりコストは抑えられるかと思います。

まとめ

AWS上にSelf-Hosted Runnerを構築することで、カスタムAMIとWarm Poolによるコストバランスのよい実行時間の短縮が実現できました。terraform-aws-github-runnerを活用することで、初期構築コストも抑えられます。GitHub-hosted runnersの限界を感じている方は、ぜひ検討してみてください。

参考リンク

DRESS CODE TECH BLOG

Discussion