Entersight

開発のこと

Github Actions の無料枠が足りなくなったので、runner だけ自前の機械に移した

2026.10.27

GitHub Actions の枠を使い切りそうになった

GitHub Actions には、非公開リポジトリで使える時間の枠があります。

プラン 1か月の枠
Free 2,000分
Pro 3,000分

小さなプロダクトなら十分に見えますが、PR に push するたびに CI が走るので、思ったより早く減ります。私たちのリポジトリで、直近40回の CI がどこで時間を使っていたかを数えました。

ジョブ 回数 合計 1回あたり
単体テスト(Vitest) 40 184分 4.6分
変更ファイルの判定 40 40分 1分
Lint(Biome) 40 40分 1分
E2E(Playwright) 1 8分 8分

分かったことは2つです。

  • 一番使っているのは単体テスト。 push のたびに走るためです。E2E は1回が長くても、デプロイのときにしか走らないので合計は小さい
  • 数秒で終わるジョブも1分ずつ使う。 GitHub はジョブごとに1分単位で切り上げて数えます

テストはこれからも増える

今は枠に収まっていても、この先は増える一方だと考えています。

E2E には、目的の違う2種類があると思っています。

  • エンジニアの視点の E2E: 作ったものが壊れていないことを確かめる。機能を足したら、その分を足す
  • QA の視点の E2E: アプリをいじめる。想定外の操作や組み合わせで、壊れる所を探す

後者は、必ずしもエンジニアが書かなくてよいはずです。書く人が増えれば、テストの数も CI の時間も増えます。単体テストも、機能が増えれば同じように増えます。

枠を節約する工夫(関係するテストだけ流す、重なった実行を止める など)もできます。ただ、テストが増えていく前提なら、上限そのものを外したくなります。

runner だけを外に出せる

GitHub Actions は「ワークフローの管理」と「ジョブを実際に動かす機械(runner)」が分かれています。runner だけを自分の機械に置くことができます(self-hosted runner)。

self-hosted runner で動いたジョブは、GitHub の枠を使いません。リポジトリも PR も Actions の画面も今のままで、ジョブが動く場所だけが変わります。

GitLab などへ移ることも考えました。ただ、自前のサーバーで動かして上限を外すことは GitHub のままでもできます。それなら、issue・PR・gh 前提の運用を作り直す理由がありません。

どうやって自分の機械と GitHub を繋ぐのか

最初に気になったのはここでした。答えは、機械の側から GitHub に取りに行くです。

  1. 機械に GitHub 公式の runner アプリを入れて、起動しておく
  2. runner は HTTPS で GitHub に「自分向けのジョブはあるか」と問い合わせ続ける
  3. ワークフローの runs-on のラベルが runner のラベルと一致すると、GitHub がそのジョブを渡す
  4. runner がコードを checkout して手順を実行し、ログと結果を GitHub に返す

通信は機械から外に出ていくものだけです。ポートを開ける必要も、固定 IP も要りません。社内ネットワークの内側の機械でも動きます。

登録は、リポジトリの Settings → Actions → Runners → New self-hosted runner に出るコマンドを流すだけです。

./config.sh --url https://github.com/<owner>/<repo> --token <TOKEN> --labels my-runner
./run.sh

ワークフローは runs-on を書き換えます。

jobs:
  unit-tests:
    runs-on: [self-hosted, linux, my-runner]

自分の PC か、専用の機械か

手元の Mac でも動かせます。ただし次の点が気になりました。

  • スリープや電源オフの間は、CI が待ちになる(別の runner に自動では切り替わらない)
  • PR のコードが自分の PC で、自分の権限で動く
  • CI の間は手元の作業が重くなる。E2E は手元の開発サーバーとポートや DB を取り合う

私たちは、社内で常時動いている機械に置かせてもらうことにしました。runner はジョブごとに新しいコンテナで動かし、終わったら消します。CPU とメモリには上限を付け、その機械の本来の仕事に影響しないようにしています。

移してみて引っかかったこと

1. コンテナの中では、CPU が1つに見えていた

移した直後、単体テストの時間が GitHub の機械とほとんど変わりませんでした。手元では数十秒で終わるテストです。

調べると、コンテナの中で次のようになっていました。

nproc=16 availableParallelism=1

nproc は機械全体の16コアを返します。一方で Node.js の os.availableParallelism() は、コンテナに掛けた CPU の上限を見て 1 を返していました。Vitest は既定で「使えるコア数 − 1」本のワーカーを立てるため、テストが1本ずつしか動いていませんでした。

CI のときだけ並列数を決め打ちして解決しました。

// vitest.config.ts
export default defineConfig({
  test: {
    maxWorkers: process.env.CI ? 4 : undefined,
  },
})

単体テストのジョブは 4.6分から 2分になりました。

ついでに、もともと書いていた poolOptions.threads の設定が効いていなかったことも分かりました。Vitest の既定の pool は forks なので、threads 向けの設定は読まれていませんでした。

2. E2E で、ブラウザの OS ライブラリが入れられない

Playwright の npx playwright install --with-deps chromium は、ブラウザが使う OS のライブラリを apt で入れます。そのために sudo で root になろうとします。runner のコンテナは安全のために root になれない設定にしてあるので、ここで止まりました。

sudo: The "no new privileges" flag is set, which prevents sudo from running as root.

--with-deps を外すとブラウザ本体は入りますが、起動できません。

error while loading shared libraries: libnspr4.so: cannot open shared object file

OS のライブラリは、ジョブの中ではなく runner のイメージに最初から入れておく必要があります。イメージを作るときに root で npx playwright install-deps chromium を流すか、Playwright 公式のイメージ(mcr.microsoft.com/playwright)を元にします。Playwright の版を上げたときは、イメージも同じ版で作り直します。

ワークフローは、GitHub の機械とどちらでも動くように分けておきました。

- name: Install Playwright browsers
  run: npx playwright install ${{ runner.environment == 'github-hosted' && '--with-deps' || '' }} chromium

まとめ

  • GitHub Actions の枠は、テストが増えるほど足りなくなる
  • runner だけを自前の機械に移せば、GitHub の使い勝手はそのままで枠を使わなくなる
  • 繋ぎ方は、機械から GitHub に問い合わせるだけ。ポートの開放は要らない
  • コンテナで動かすときは、CPU の数の見え方と、OS ライブラリの入れ方に気を付ける
← ブログ一覧へ