Bot mmnot 環境構築・インフラ 開発ログ

🛠️開発記録#240(2025/6/1)Self-hosted Runner の罠と向き合う──macOS + GitHub Actions + Docker の実践ログ

はじめに

私は現在、macOS 上で仮想通貨の自動取引 Bot を開発している。
複数の戦略を同時並行で実験・運用することを前提とした基盤構築が進みつつあり、その一環として今回、GitHub Actions の Self-hosted Runner を本格導入することになった。

ただ、導入してすぐに思い通りに動くほど甘くはなかった。
Runner が起動しているにも関わらず、ワークフローが突然停止。ログを見ると「Node バイナリが見つからない」という不可解なエラー。さらに docker login を行うステップでは、macOS 特有の Keychain 制限に引っかかって認証エラーが発生。

手元で開発している環境と、バックグラウンドで動いている Runner の セッション種別やセキュリティ層の違いが表面化し、これまで何気なく使っていた構成が実は非常に繊細なバランスで動いていたことを痛感させられた。

今回の記事では、

  • Self-hosted Runner を 用途別に分離する構成
  • macOS 上で Runner を 安定して常駐させる方法
  • Keychain 問題を回避して GHCR 認証を通す実践的な手法
    など、開発と運用の“あいだ”にある技術課題をどう乗り越えたかを記録しておく。

これは単なるトラブルシュートの記録ではない。
Bot 開発というプロダクトの“中身”を支える CI基盤をどう設計し、どう育てていくか──その試行錯誤の一端でもある。

なぜ Runner を分離したのか?

GitHub Actions の Self-hosted Runner を導入する際、最初はひとつの Runner にすべてのジョブを詰め込んでいた。
E2E テストも、バックテストも、通知スクリプトも、1 台の Runner がすべてを担っていた。動かないわけではない。ただ、この先 Bot の数が増え、ジョブの種類も増えていく中で、明確な限界が見えていた。

ラベルで用途を分離するという発想

GitHub Actions では Runner に ラベル(labels:)を自由に付けることができる。
この仕組みを使えば、たとえばこういう運用が可能になる:

  • self-hosted, health → ヘルスチェック系のジョブを受け持つ Runner
  • self-hosted, e2e → E2E テストだけを受け持つ Runner
  • self-hosted, frbot → FR ヘッジ Bot 用のRunner(将来)

こうして ラベルでワークフローをルーティングできるようになると、Botの系統が増えたときに「どの Runner が何のジョブを担当するか」が視覚的に、構造的に把握しやすくなる。

並列性と安定性の確保

Bot 開発においては「バックテスト」「E2E」「Slack 通知」「本番運用」が 同時多発的に動く可能性がある
そのときに、ひとつの Runner にすべてのジョブを押し込むと、以下のような事態が起きる:

  • 他ジョブに巻き込まれて本番系ジョブの開始が遅れる
  • ログの行き先が混ざって解析しづらくなる
  • 「今この Runner は何をしているのか」が把握できなくなる

そのため、ジョブ単位の分離 = Runner の分離という発想は、Bot の安定稼働を支える基盤設計として必須になる。

Runnerが動かない!?初めて直面した node20 エラー

Runner のラベル設計も済み、E2E テスト専用の mac-e2e Runner を用意し、いよいよワークフローを流そうとしたとき――
最初に待ち受けていたのは、actions/checkout@v3 がいきなり失敗するという意外な事態だった。

ログを確認すると、そこにはこんなエラーが出ていた:

Error: An error occurred trying to start process '/Users/xxx/actions-runner/externals/node20/bin/node'
No such file or directory

最初は意味がわからなかった。actions/checkout が node を使っている? node20 が消えている?
ここで初めて、GitHub Actions Runner の内部構造と向き合うことになった。

Self-hosted Runner の内部には Node.js バイナリが含まれている

多くの GitHub Actions(checkout, setup-python, upload-artifact など)は Node.js で実装されている。
Self-hosted Runner はそれらを実行するために、自分自身の中に externals/node20/bin/node という Node.js 実行環境を内蔵している

つまり、Runner の内部にある node が壊れていたり消えていたりすると、GitHub Actions のほとんどが動かないという構造になっていたのだ。

原因は、自分自身による Runner フォルダの破損

思い返せば前日、自作スクリプトで Runner ディレクトリに対していくつかのテストを行っていた。その際に externals フォルダを消してしまった可能性が高い。
健康な Runner(health-check 用)から node20 をコピーしようとしたが、そちらにも同じバイナリが存在しないことが判明。

解決策:公式 Runner tarball から node20 を復元

最終的には、GitHub 公式から配布されている macOS 用 Runner の .tar.gz をダウンロードし、
その中から externals/node20 フォルダだけを取り出して復元した。
node --versionv20.x.x が表示されることを確認し、ようやく actions/checkout@v3 が正常に動き出した。

この一連の流れで得た教訓はひとつ:

Runner はただの実行エージェントではなく、Node.js バイナリを内包した “小さなランタイム環境” である

それが壊れれば、GitHub Actions の核そのものが崩れる。
つまり、「Runner を正しく管理する」というのは「GitHub Actions を正しく動かすための最も根本的な責任」でもあるのだ。

新たな Runner を作り直す

Node バイナリの欠損が明らかになった時点で、私はひとつの決断を下した。
**「既存の Runner を修復するより、新たに構成し直したほうが確実だ」**という判断だ。

/Users/xxx/actions-runner-e2e に新しい Runner を構築

まず、macOS 上のホームディレクトリに actions-runner-e2e というフォルダを新たに作成。
ここに GitHub 公式の Runner パッケージ(Apple Silicon 用の arm64 バージョン)をダウンロードし、展開。
続いて config.sh を実行して、GitHub 上のリポジトリとこの Runner を接続した。

./config.sh \
  --url https://github.com/<username>/<repo> \
  --token <registration-token> \
  --name mac-e2e \
  --labels self-hosted,e2e

この --labels によって、この Runner は E2E テスト専用の実行エージェントとして定義される。

Launchd で macOS 常駐サービスとして起動

Runner を手動で ./run.sh から起動することもできるが、今回は macOS の Launchd を使って自動常駐化する方針を選んだ。

./svc.sh install
./svc.sh start

これによって Runner は macOS 起動時に自動で立ち上がるようになり、
またログインユーザーのセッションに依存しない、より本番運用に近い構成を実現できた。

mac-e2e Runner の誕生

こうして /Users/xxx/actions-runner-e2e に配置された Runner は、
GitHub 上では mac-e2e という名前で識別され、
[self-hosted, e2e] ラベルの付いたワークフローを専門に受け持つことになった。

この時点でようやく、E2E テストを専用の Runner で回すための「環境的な土台」が整ったわけだ。
だが、次に待っていたのは macOS 特有の “罠” だった。

Keychain の罠──macOS で docker login が落ちる理由

Runner が無事起動し、E2E ワークフローも mac-e2e に割り当てられるようになった。
次は Docker イメージをビルドし、コンテナベースでテストを走らせる――そのはずだった。

だが、次のステップでワークフローはまたしても 赤く染まった

Error saving credentials: error storing credentials - err: exit status 1,
out: `User interaction is not allowed. (-25308)`

エラーの出所は docker login ghcr.io のステップ。
GitHub Container Registry(GHCR)へのログインを試みた際に、macOS の Keychain API にアクセスできず失敗していた

なぜ GUI の Keychain にアクセスできないのか?

その理由は Runner の起動方式にある。
今回構築した mac-e2e Runner は、macOS の Launchd を使って バックグラウンドサービスとして起動している。
この Launchd は **GUI セッションに紐づかない「非対話的なセッション」**であるため、
macOS の Keychain(=GUIユーザー専用のセキュリティストア)にアクセスすることができない。

結果として、docker login は認証自体には成功するものの、認証情報を保存しようとして Keychain に触れた瞬間にエラーになるという現象が起きていた。

解決策:Keychain を完全にバイパスする

この問題に対処するため、私は docker login を使うのをやめ、
代わりに DOCKER_CONFIG を一時的に切り替えて、認証情報を Base64 形式で直接書き込む方針を採った。

export DOCKER_CONFIG=$(mktemp -d)
AUTH_B64=$(echo -n "${{ github.actor }}:${CR_PAT}" | base64)

cat > "$DOCKER_CONFIG/config.json" <<EOF
{
  "auths": {
    "ghcr.io": {
      "auth": "${AUTH_B64}"
    }
  }
}
EOF

echo "DOCKER_CONFIG=$DOCKER_CONFIG" >> $GITHUB_ENV

こうすることで、Keychain を一切経由せず、
GitHub Actions の中だけで完結する セキュアかつ macOS 非依存な認証処理が実現できた。

macOS を使った Runner 構築で得た大きな教訓

macOS の GUIセッションで当たり前にできていたことが、Launchd では一切通用しない

これは今回 Runner を常駐サービスとして設計したからこそ、初めて表面化した問題だった。
Keychain のような GUI に依存する仕組みを「当然使えるもの」として捉えていると、
バックグラウンド環境での運用に切り替えた瞬間に破綻することがある。

macOS 上で開発と本番を分けて運用する場合には、この “セッション非対称性” を深く理解しておくことが重要だ。

成功!Live E2E ワークフローのグリーン化

Runner の分離、Node バイナリの復旧、Keychain をバイパスした GHCR 認証――
あらゆる障害を乗り越えて、ついに Live E2E ワークフローが 緑のチェックマークを灯した。

ジョブは mac-e2e Runner に割り当てられ、
docker compose build で Bot のイメージをビルドし、
テストスクリプトである realnet_checks.sh が本番環境の設定下で正常に実行される。

jobs:
  live-e2e:
    runs-on: [self-hosted, e2e]

たった1行のこの記述の裏に、Runner構成の設計・認証方式の切替・実行環境の分離といった、
いくつもの“見えない技術的レイヤー”が折り重なっている。

Slack 通知とログアップロードも統合

テストが成功しても、失敗しても、Slack に自動通知が飛ぶようになっている。
また、ログファイルは GitHub Actions のアーティファクトとして保存され、
失敗時の分析や再現にも役立てられるようになっている。

- name: Upload E2E logs
  uses: ./.github/actions/upload-artifact
  with:
    name: live-e2e-logs-${{ github.run_id }}
    path: logs/live-e2e.log

これにより、「定期実行されたテストがどうだったのか」が
自動的に通知され、あとから振り返ることもできるという「安心できる運用ループ」が確立された。

“構築したRunnerで、はじめて本番相当のE2Eテストが成功した”という重み

このグリーンは単なる通過点ではない。
Runner 設計・環境分離・エラー対応すべてを経て、開発環境と運用環境が一体となって回り出したという証でもある。

そして何より、自作のBotがただ動くだけでなく、
「安全に、継続的に、確認されながら動く」状態を手に入れたという意味で、大きな一歩だった。

今後に向けた展望

今回の構築とトラブルシューティングを通して、Self-hosted Runner の仕組みや macOS の制約を深く理解できた。
同時に、この開発基盤はあくまで“はじまり”であり、“育てていく対象”であるという感覚が明確になった。

中・長期的にはクラウド展開を視野に

現在はローカルの macOS 上で Runner や Bot を稼働させているが、
今後は徐々にこれらを クラウド(VPS / Cloud Run / Kubernetes)へ移行していく予定だ。

ただしそれは、“いきなりクラウドに全部投げる”という意味ではない。
むしろ今の開発構成(docker-composeベース)をローカルでしっかりと動かしながら、

  • .env.production.env.testnet で環境変数を切り替えられるようにする
  • Botごとにコンテナ設計を分離し、用途ごとの Runner に割り当てられるようにする
  • Secrets やログ保存・通知などを一貫して扱える構造にする

というふうに、ローカル環境で本番相当の構成を“仮想的に再現”しておくことが、クラウド移行の一番の近道になると実感した。

Runner 自体の Docker 化、Kubernetes での統合運用も視野に

Runner を直接 macOS 上で動かすのではなく、
Docker コンテナ化された Runner をクラウド上で複数稼働させる構成に切り替えることで、

  • Bot の開発・テスト・本番運用の「全ての系統」をクラウド上で一元管理
  • Kubernetes によるオートスケーリングや自動再起動・監視との統合
  • 開発者自身は ChatGPT や Cursor、GitHub Actions などのツール群を変えずに運用だけ切り出す

といった より本番志向な CI・運用体制へと発展させられる。

この構想は、個人開発であっても十分に現実的だ。
むしろ個人だからこそ、**一貫した設計思想とツール整備によって「ひとりSRE的な安心感」**を得ることができる。

まとめ:開発と運用の“間”にある技術を鍛える

今回の取り組みを振り返ると、「Botをつくる」だけでは終わらない、
そのBotを“ちゃんと回す”ための技術と思想を鍛える時間だったように思う。

コードが書けるだけでは足りない。
動作確認ができるだけでも不十分。
“自動で・継続的に・安全に動き続ける”という状態をつくるには、
その“裏側”を支える構造=CI/CD・Runner・環境管理の知識
が不可欠だった。

特に印象的だったのは、macOS の Keychain に阻まれたときだ。
「ローカルで動いているから本番でも動くだろう」という甘い想定が崩れ、
セッション種別の違い・Launchd の特性・Keychain API の仕様など、
OS の深層構造に一歩踏み込まざるを得なかった。

でもだからこそ、今自分が持っているRunner基盤は、
仕組みごと“理解して育ててきた”手応えのある土台になっている。


この一連の経験は、「Botを動かす技術」を超えて、
“Botを運用する技術”を学び始めた瞬間でもあった。

Botが増えるほど、戦略が高度になるほど、
開発と運用の「間」にあるこの技術層はますます重要になる。

自分の手でRunnerを分離し、構成を整理し、障害を突破して、
ようやくたどり着いた“緑のチェックマーク”の意味は、想像以上に重かった。

そしてこれは、まだ序章にすぎない。
この先には、クラウドへの展開、Kubernetes化、マルチBot運用が待っている。

その第一歩として、今回の取り組みは、確かな意味と手応えを持っていた。

-Bot, mmnot, 環境構築・インフラ, 開発ログ