この記事でできること
privateリポジトリでGitHub Actionsを使っていると、無料枠の上限に当たります。上限に当たるとジョブが起動すらしなくなるので、CIも自動デプロイも止まります。
対処の1つが、ジョブをセルフホストランナー(自分の手元のマシンやサーバーに置くランナー)へ移すことです。課金対象から外れるので、上限を気にせず回せるようになります。
ただ、runs-on を書き換えれば移せる、とはいきませんでした。GitHub-hosted runnerを前提に書かれたワークフローには、GitHub側の環境に依存している部分がいくつも紛れ込んでいるからです。
この記事は、個人開発している技術記事キュレーションサービスDevPickでCI・自動レビュー・デプロイをセルフホストランナーへ移したときに、実際に動かなくなった4箇所とその直し方をまとめたものです。
前提: private/publicで話が変わります
先に注意点です。publicリポジトリでセルフホストランナーを使うのは避けてください。 誰でもPRを出せる以上、そのPRのコードが自分のマシン上で実行されうるためです。GitHubの公式ドキュメントでも推奨されていません。
この記事はprivateリポジトリを前提にしています。無料枠の上限に当たるのも主にprivateリポジトリのほうです(publicリポジトリはActionsの実行時間が無料のため)。
直す箇所1: actions/setup-* がランナーのOSに対応していない
移行して最初に落ちたのがここでした。actions/setup-python が「指定したバージョンが見つからない」というエラーで即座に失敗します。
このActionは、指定されたバージョンのランタイムをダウンロードしてくる仕組みです。ダウンロードされるのはGitHub-hosted runner向けにビルドされた汎用Linux版で、それ以外のディストリビューションには配布がありません。手元のランナーがAlmaLinuxだったため、対応するビルドが存在せず落ちていました。
直し方は、ダウンロードに頼らず、ランナーに入っているランタイムを直接使うことです。
- ランナー側にパッケージマネージャで必要なバージョンを入れておく
- ワークフローからは
actions/setup-pythonを外し、システムのPythonを直接呼ぶ - 依存関係はシステム側を汚さないよう、venvなどに切り離してインストールする
セルフホストランナーは環境が使い回されるので、「毎回ランタイムを用意する」という発想自体が要らなくなります。ランナーの構築時に一度入れてしまうほうが素直です。
直す箇所2: キャッシュ系の処理がハングする
次に踏んだのがこれです。actions/setup-node に cache: npm を付けていたのですが、セルフホストランナー上でジョブがハングして進まなくなりました。デプロイが完了しないまま止まるので、影響としてはこちらのほうが厄介でした。
原因はGitHub Actionsのキャッシュサービスとの通信です。エラーで即座に落ちてくれればまだ気づけるのですが、応答を待ち続けるので「ずっと実行中」に見えます。
そもそも、セルフホストランナーではキャッシュの復元自体がほとんど不要です。ワークスペースが毎回作り直されるGitHub-hosted runnerと違い、前回の node_modules や仮想環境がそのまま残っているからです。キャッシュから戻すために外部と通信するくらいなら、何もしないほうが速いことになります。
cache: npm の指定と actions/cache のステップは、まとめて外しました。
副産物として、PRをクローズしたときにそのブランチのキャッシュを削除するワークフローも役目を失いました。Actionsのストレージ無料枠を空けるために入れていたものですが、キャッシュを作るジョブが無くなったので削除しています。節約のために足した仕組みは、前提が変わったら一緒に消す必要があります。残しておくと、何のために存在するのか分からないワークフローが増えていきます。
直す箇所3: サービスコンテナのポートが手元の開発環境と衝突する
テスト用にRedisやMySQLをサービスコンテナで立ち上げているワークフローは要注意です。
GitHub-hosted runnerは毎回まっさらな環境なので、標準ポートをそのまま使って何も起きません。一方、セルフホストランナーが自分の開発機だと、同じポートで開発用のRedisがすでに動いています。ここが衝突しました。
サービスコンテナ側のホストポートを、標準からずらした番号(Redisなら6379ではなく16379など)に割り当て、テストからはそちらを見るようにして解決しています。
「そのマシンで他に何が動いているか」を意識する必要がある、というのがGitHub-hosted runnerとの一番大きな違いかもしれません。使い捨ての環境という前提が消えるので、ポートに限らず、ファイルの置き場所や環境変数も同じ観点で見直す価値があります。
直す箇所4: 実行ユーザーが変わって権限で落ちる
これはデプロイ処理を移したときに出ました。
ビルド成果物のtar.gzをサーバー側で受け取り、一時ファイルに書き出してから、アプリ実行用のユーザーに切り替えて展開する、という流れになっていました。ところがこの展開が Permission denied で失敗します。
root権限で一時ファイルを作ると、パーミッションは600(所有者だけが読み書き可能)で、所有者はrootになります。その後 sudo -u で別のユーザーに切り替えると、そのファイルが読めません。作った側と読む側でユーザーが違っていた、というだけの話です。
一時ファイルを作った直後に、読み取り可能なパーミッションを明示的に付けて解決しました。
移行時にこの手の権限エラーが出やすいのは、GitHub-hosted runnerでは基本的に単一ユーザーですべてが完結するのに対し、自前の環境では「CIを実行するユーザー」「アプリを動かすユーザー」が分かれていることが多いためです。ファイルを作るステップと使うステップでユーザーが変わっていないか確認してください。
移行前にやっておくとよいこと
上の4つとは別に、移行前後で入れておいてよかった設定が1つあります。同じPRへ連続でpushしたときに、古いCI実行をキャンセルする設定です。
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
これが無いと、pushのたびに古い実行が最後まで完走します。GitHub-hostedなら無料枠の消費、セルフホストなら自分のマシンの占有につながります。なお、main へのpushで動くデプロイのようにコミットごとに意味があるジョブは、キャンセルされると困るので対象から外してください。
まとめ
-
actions/setup-*はランタイムをダウンロードする仕組みなので、GitHub-hosted runner向けの配布が無いディストリビューションでは動かない。ランナー側に入れて直接使う - キャッシュ系の処理はGitHubのキャッシュサービスと通信するため、セルフホストではハングすることがある。ワークスペースが永続するのでそもそも不要。キャッシュのために足していた掃除用ワークフローも一緒に消す
- サービスコンテナのポートは、そのマシンで動いている開発環境と衝突する。ずらした番号に割り当てる
- ファイルを作るステップと使うステップで実行ユーザーが違うと権限で落ちる。自前の環境ではユーザーが分かれている前提で確認する
- publicリポジトリではセルフホストランナーを使わない
そもそもなぜ移行することになったのか(無料枠が切れてデプロイが止まり、本番反映が滞留した経緯)はZenn版にまとめています。