GitHub Actionsの月間無料枠が枯渇しがちなので、WSL2のセルフホストランナーに移行した話
概要
今回はGitHub Actionsの月間無料枠が足りなくなったので、手元のWSL2にセルフホストランナーを立てて、実行場所だけを自分のPCに切り替えたという話を紹介していきます。
自分のブログは、記事のPRを作るとレビューの一部が自動で走り、マージすると日付確定と本番デプロイまで走る、という流れをGitHub Actionsで組んでいます。
便利なのですが、レビューでClaudeを長く走らせるぶん、実行時間(分数)もそれなりに食います。
そのうえ月間の無料分数は、リポジトリ単位ではなくアカウント単位で共有されます。
privateリポジトリが複数あると、思った以上に早く上限が来ます(- -;
そこで選択肢をいろいろ出してもらった結果、自分の環境に一番合っていたのがセルフホストランナーでした。
セルフホストランナーとは、GitHubのサーバーの代わりに、自分のPCでジョブを実行してくれる仕組みです。
ワークフローの中身はほぼそのままで、書き換えはruns-onの1行だけで済みます。
以降、GitHubが用意しているサーバーを「クラウド」、自分のPCのランナーを「手元」と呼びます。
登録の途中で「別のリポジトリ向けの画面を開いていた」というミスもしたので、それも含めて書いていきます。
それではやっていきましょう!
目次
なぜセルフホストランナーにしたのか
無料枠の問題に対して、まず思い付いた選択肢は次のとおりです。
- Spending limit(課金上限)を引き上げる
- 設定だけで済むが、そもそも課金したくない
- リポジトリをpublicにする
- publicリポジトリの標準ランナーは無料。ただし記事の下書きやSecretsの扱いが公開前提になるので見送り
- 他のCI(GitLab CIなど)に移す
- 移行コストが大きいわりに、Claudeを走らせる分の実行時間は減らない
- 手元のPCで実行する(セルフホストランナー)
- 分数が一切かからない。ワークフローの書き換えもほぼ不要
自分の場合は、次の3つの条件がありました。
セルフホストランナーなら、どれも満たせます。
- PCが常時起動ではない
- PCが止まっている間、ジョブはGitHub側で待機する。PC(WSL2)が起動してランナーが立ち上がれば、そのまま実行される
- 運用は無料にしたい
- 手元のPCで動かすので、GitHubの無料分数を消費しない
- Claudeのトークン消費は増やしたくない
- Claudeの認証はSecretsのOAuthトークンで渡している。実行場所が変わっても、消費は変わらない
特にPCが止まっているときも、待機したジョブは取りこぼさず、起動後に実行されます。
ただし約24時間待つと失敗扱いになります。
結論として、この3条件をすべて満たせたのは、セルフホストランナーだけでした。
全体の流れ
やることは大きく3つです。
- WSL2にランナーを登録して、systemdのサービスにする
- ワークフローの
runs-onを切り替え可能にする - リポジトリ変数でクラウドと手元を切り替える
実行の流れは次のようになります。
- 記事のPRを作る、またはマージする
- ここまでは今までと同じ。GitHub側でジョブがキューに積まれる
- WSL2のランナーがジョブを受け取る
- ランナーはGitHubに常に問い合わせているので、ポート開放などは不要
- 手元のPCでレビューやデプロイが走る
- 結果はこれまでどおりGitHub上のPRやコメントに出る
ランナーの登録手順
登録用のコマンドを取得する
対象リポジトリの「Settings」→「Actions」→「Runners」→「New self-hosted runner」を開きます。
OSはLinux、アーキテクチャはx64を選ぶと、ダウンロードから登録までのコマンドが表示されます。
ここで開いているのが、ワークフローを動かしたいリポジトリの画面かを必ず確認してください。
自分は最初、別リポジトリの画面を開いていて、config.shの--urlが違うことに気付いて開き直しました。
このまま登録すると、対象リポジトリのジョブがランナーを見つけられず、ずっと待機になります。
ダウンロードして登録する
WSL側でフォルダを作り、表示されたコマンドを順に実行します。
バージョンやハッシュ値は画面に表示されたものを使ってください。
ランナーの取得と登録
1 | mkdir actions-runner && cd actions-runner |
--tokenは画面に表示される登録用の短時間トークンです。
一度使うと再利用できません。
systemdのサービスにする
config.shが終わっただけでは、ランナーのプロセスは動いていません。
実際にこの時点でGitHubのRunners画面を見ると、ステータスはOfflineでした。
WSL2でsystemdを有効にしておけば、付属のsvc.shでサービス化できます。
自分の環境は/etc/wsl.confに次の設定が入っていました。
/etc/wsl.conf
1 | [boot] |
あとはランナーのフォルダでsvc.shを使ってサービス化します。
root権限が必要なのでsudoを付けます。
サービス化
1 | cd ~/actions-runner |
statusでactive (running)と出て、enabledになっていれば成功です。enabledなので、WSL2が起動するたびにランナーも自動で立ち上がります。
GitHubのRunners画面で、Idleと表示されれば準備完了です。
ワークフローを切り替え可能にする
全ジョブをself-hostedに固定してしまうと、PCが使えないときに困ります。
そこでruns-onを、リポジトリ変数で切り替えられる書き方にしました。
ワークフローのrunsを変更
1 | # 変更前 |
これをワークフロー全ジョブに入れました。
変数RUNNERが未設定なら従来どおりubuntu-latest(クラウド)で動き、RUNNER=self-hostedにすれば手元のランナーで動きます。
リポジトリの「Settings」→「Secrets and variables」→「Actions」→「Variables」タブを開いて、下側の「Repository variables」に追加します。
変数の画面には「Environment variables」と「Repository variables」の2つがあります。
ワークフロー全体で使うvars.RUNNERは、下のRepository variablesに登録します。
Environment variablesは、特定の環境(Environments)を指定したジョブでしか読めません。
先にruns-onの変更をmainへマージしておけば、変数を追加した瞬間から切り替わります。
変数が未設定のあいだは今までと挙動が変わらないので、自分は変更のPRを先にマージしてから変数を追加しました。
動作確認
PRをマージすると、日付確定とデプロイを行うワークフローが起動します。
これが手元のランナーで動くかを確認しました。
手元で動いたかは、次の2つで確認しました。
- GitHubのAPIでジョブの情報を見ると、
runner_nameに登録したランナーの名前が出ていた(クラウドではない証拠) - WSLのjournalにも
Running jobのログが出ていた
結果は、日付確定(finalize-date)も本番デプロイ(deploy)も、両方とも成功でした。
ただし初回は思ったより時間がかかりました。
リポジトリのcheckoutやNode.jsのセットアップで、ツールのダウンロードとキャッシュ作りが走るためです。
2回目以降は_workフォルダにキャッシュが残るので、もう少し速くなるはずです。
気を付けたい点
- WSL2が止まっている間はジョブが動かない
- ジョブは待機になり、WSL2が起動すると実行される。待機は約24時間まで
- 止まっているかはRunners画面で分かる(
Offlineと表示される)
- 実行中にWSL2が落ちるとジョブは中断される
- VSCodeを閉じるなどでWSL2が止まると、長いレビューは途中で失敗する。Actions画面から再実行する
- Secretsが手元のPC上に展開される
- privateリポジトリ専用で使う。フォークPRが動くようなpublicリポジトリでは避ける
git config --globalで書いた設定が、PCに残る- 詳しくは次の体験談のとおり
git config –globalの設定がPCに残った話
最後の項目は、自分が実際にハマったので少し詳しく書きます。
何が起きたか
デプロイ用のステップが、git config --globalで「本番リポジトリ用のトークンを使う設定」を書いていました。
クラウドのランナーは実行後に環境ごと捨てられるので、今までは問題になりませんでした。
手元だと、この設定が~/.gitconfigにそのまま残ります。
どう困ったか
その状態で別のリポジトリにgit pushしたところ、本番用のトークンが使われて403になりました。
どう直したか
設定を--globalで書くのをやめて、環境変数で渡すようにしました。
環境変数なら、そのステップの中だけで有効になります。
直したあとは、実行後も~/.gitconfigに何も残らなくなりました。
GIT_CONFIG_*で渡す例
1 | env: |
GIT_CONFIG_COUNTが設定の数、GIT_CONFIG_KEY_nとGIT_CONFIG_VALUE_nがn番目のキーと値です。
例ではuser.nameですが、トークンの書き換え設定も同じ形式で渡せます。
認証情報を含む設定ほど、--globalで書かずに環境変数で渡すようにすると安全です。
締め
レビューのようにClaudeを長く走らせるワークフローは、待ち時間がそのまま分数になるので、無料枠が想像以上に早く減ります。
実行場所を手元に移すだけなら、ワークフローの書き換えはruns-onの1行で済みました。
登録の画面を開き間違えたり、svc.shの実行が後回しになってOfflineのまま気付かなかったり、デプロイ後にgit pushが403になったりと、細かいところでは少し手こずりました。
それでも、PCさえ動いていれば無料で回せるのは大きいです。
以上となります。
月末に無料枠の残りを気にしなくてよくなるのは、気が楽です(^^
それではお疲れさまでした。

