GitHub Actions Runner を手元の MacBook Pro で動かす話

GitHub Actions Runner を手元の MacBook Pro で動かす話

AIがコード量を増やしたことで、CIのe2eテスト実行時間が悩みの種になってしまいました。AWS上でのランナー構築を検討した結果、最終的には手持ちのMacBook ProをLimaで仮想化し、セルフホストランナーとして活用する方法にたどり着きました。その実装と工夫を紹介します。
2026.08.06

世界は因果で成り立っています。AI エージェントが実装をして、1日にかけるコード量は増えました。その結果、コードレビューをするのが難しくなりました。なので、レビューで担っていた役割をハーネス、テスト、ADRなどで補うようになりました。テストが増えるとPRで発火するCIの時間も長くなります。

つまり何が言いたいか、AIがコードを書くようになって困ったのは実装ではなくCIでした。静的解析と型チェックくらいなら可愛いんですが、e2e となると並列で回しても1つあたり5-10分かかります。実装量が増えた分だけコレがのしかかってくる訳ですから、それはもう一瞬でした。

このままでは月末までCIが回せずに困ったことになるので、色々検討した結果、自分のMacBook Pro を CI ランナーにするに至ったのでそのあたりのことをまとめてみました。

環境について

詳しくは書けませんが、Next.js で動くアプリが2つとHono で動くワーカーが1つ入ったモノリポです。アプリの1つは現状こんな感じです。

TS / TSX ファイル 303
重めのテストファイル 47(うち E2E spec 21・DB 統合 12)

CI で実行している内容が結構重めです。つまり 1回の CI で、DB を作り直して 5万件流して、Next を 2本ビルドして、ブラウザを立ち上げています。
書いてみてそりゃ利用枠ガンガン消費しますよねぇで感想文コンクール優勝狙えます。

  • Biome・TypeScript の型チェック・境界値チェック・ユニットテスト
  • Next.js の本番ビルド(2つ分)・Hono のバンドルチェック
  • postgres コンテナでの migrartionとseedの実行
    • 結合テスト約80件・スケールテスト
  • 1アプリに対して e2e を2シャードに分割して dev サーバーで実行

この定義とは別に、GitHub の機能である CodeQL も毎push ごとに並走していました。1回あたり2分ですが、チリも積もればなんとやらです。

self-hosted Runner で不採用だったもの

まずは机上でパッと思いつく選択肢を検討しました。

AWS CodeBuild managed GitHub Runner

AWS CodeBuild はGitHub Runner をフルマネージドでサポートしています。

https://docs.aws.amazon.com/codebuild/latest/userguide/action-runner-overview.html

ですが、ジョブごとに新しい環境が立つためPlaywright のブラウザも .next/cache も npm キャッシュも都度コールドスタートになります。そのため、1回の実行が30分前後になる推定です。なので、仮に1日に20回実行してたら月に200USD かかる可能性があります。Lambda Compute は安いのですが、Docker デーモン非対応とのことで使えませんでした...。

ECS で Runner を都度起動する

DeNA 様が Amazon ECS で作るスケーラブルなセルフホストランナー という資料を公開していました。これをベースに ECS のタスクをCI 都度立ち上げることを検討しました。

まず、資料に挙げられた課題の「ランナーが利用できるまでに最短 30秒」と「apacity Provider のスケールアウトが数分」が致命的で、業時間帯は常駐ランナーで埋めていました。
ただ、常駐すると使ってない時間が勿体無かったり、そもそも作業が立て込んだ時に少し厄介という印象を受けました。

また、この方法の弱点はキャッシュを保持することができないことです。EFS でマウントすれば良さそうに見えますが、

  • npm ci は 10 万個規模のファイル操作をする。
    • EFS の 1 操作あたりのレイテンシはローカル SSD の 10 倍以上あり、キャッシュの方が遅い
  • Elastic Throughput は読み取りに GB 単価がかかる。1 ジョブ 1〜2 GB × 4 ジョブ × 20 run/日 = 月 4〜5 TB で、それだけで月 $100 を超えそうな試算
  • Docker のレイヤーキャッシュは EFS に置けない

結果的に暖かいローカルディスクを持った常駐ホストが要件に合うところまではこれで判断ができました。

Amazon Lightsail でインスタンスを立てる

常駐ホストが必要...となるとまずは EC2 がパッと出てきますが、月額固定で使い放題なサービスがAWS にはあります。そう、Lightsail です。$24 の インスタンス(4 GB / 2 vCPU / 80 GB)でとりあえず1ジョブずつ直列で実行する分には事足りそうですよね。便利じゃんって私も思いました。

Runnerとしてすぐに登録して CI を実行してみました。いつもはすぐ終わるビルドなどのチェックに10分以上かかりました...。これは何かおかしいと思い調べてみると、Lightsail がバーストしてました。起動時の初期化段階でバーストしてそのまま0に張り付いたまま CI が回ってます。

BurstCapacityPercentage #起動 10 分くらい
03:07  0.06 %
03:12  0.0  %
03:17  0.0000174 %
03:22  0.000103 %
03:27  0.0000687 %

Lightsail の意外でもない落とし穴、そう言われりゃそうだった、にハマりこの構成も諦めました。EC2 インスタンスを立てることも考えましたが、業務中だけの起動でもそこそこのお値段になること。業務時間が固定じゃないので値を決めずらいこともあってこちらもやめました。

解決策:MacBook Pro を使う

コンピューティングリソースをあとはどこから払い出すか頭を捻らせました。PCからGitHub に push して、そこからCIを動かして...手元にコンピューティングリソースがあることに気づきました。M2 Pro / 10 コア / 16 GB の、自分自身 MacBook Pro を CI のRunner として使えるはずです。しかも今まで用意しようとしてたハコより良いスペックをタダで。

macOS の制約として、「APFS が大文字小文字を区別」しないということがあるので、import パスの大小ミスに気付けなくなります。また、services: ブロックは Linux Runner 専用で、このブロックを私のCI では使っています。

なので、Lima を使い、Ubuntu のVM を立ててその中で Runner を動かす構成にしました。--plain で作るとホストのファイルシステムを一切マウントしないので、ワークフローが VM 内で root を取っても Mac 側には届きません。消すのも limactl delete -f ci で一発です。

limactl create --name=ci --plain \
  --cpus=6 \
  --memory=8 \
  --disk=60 \
  template://ubuntu-24.04

あとは、ブートストラップをしてあげればRunner として稼働してくれます。

ちょっとした問題と解決策

ポートの衝突

e2e のテストを2並列で実行したら一方だけおちました。 port is already allocated なので、ポート番号が衝突してしまいました。ポートを使ってる箇所は2つなので、それらが自由に選択できるようにe2eのセットアップを書き換えました。

  • Postgres は publish 先を Docker に選ばせる
  • Next.js の実行はポートを自由に選ばせる

apt が衝突した

ブルーノマーズさんもびっくりです。これも1マシンで2つのジョブを並列で実行していることが原因です。npx playwright install --with-deps chromium は中で apt-get を呼びます。2 つのシャードが同じマシンで同時に走るので、ここでぶつかっていました。

E: Could not get lock /var/lib/apt/lists/lock. It is held by process 16254 (apt-get)

flock に替えて同じ実験をすると両方 exit code 0 で終わったので、そちらを採用することで回避しました。

開発者が増えた時の対応

全員が同じラベルで Runner を立てると、GitHub は空いている Runner に配ります。そのため、他人のPR で自分のファンが回ります。そもそもPC がスリープ状態だとCI が待ちになること、コンピューティングリソースが使えないタイミングが不定期に訪れてしまうことが問題だと思ったので、PR の作者のマシンに配るようにしました。

runs-on: [self-hosted, linux, "owner-${{ github.actor }}"]

各自の Runner は owner-<GitHubログイン>playwright の 2 つを名乗ります。CI は本人へ、deploy と cron は共通ラベルで誰かが拾う。後者はどうにかするのが難しいので一旦誰かがという状態にしていますが、デプロイに時間がかかってもいいなら、先ほど不採用にした案を部分的に採択することも考えています。今やってるプロジェクトの開発者が私だけなのでしばらくは問題にならないのでこのような運用にしています。

金額と速度の試算

表にするとこんな感じです。コスト、速度的にもローカルのPC でやるのがしばらくは良さそうです。

GitHub-hosted Lightsail $24 Mac + Lima VM
check 143 s 665 s 60〜85 s
db-tests 249 s 失敗 213 s
e2e / シャード 254 s 175 s
月額 $0.16/run(枠超過後) $24 $0
1 日 70 run の月額 約 $336 $24 $0

まとめ

固定料金と持続性能は別物で、burstable なインスタンスと CI はあまり相性が良くなさそうです。月額だけ見て選ぶと 4.6 倍遅い箱を掴むことになります。
そして、CI が動いていない期間は静かに借金が溜まります。Actions が止まってから数日放置しましたが、その間の負債は動かし直した初日に露呈しました。

結果として、最もコストパフォーマンスが高かったのは新しいインフラではなく、普段開発に使っている MacBook Pro でした。

CI が重くなり、GitHub-hosted Runner の利用枠に悩んでいるなら、まずは手元のマシンを Runner にしてみる価値は十分あります。

この記事をシェアする

関連記事