今回は「セルフホスト Runner × プロキシ地獄を突破して CI を安定稼働させた話」です。
よし。セルフホスト + 企業ネットの制約下でも CI/CD をフルパスにすることができた。
— よだか(夜鷹/yodaka) (@yodakablog) May 28, 2025
前回 #237 (2025/5/26) では「壊さず早く回す」基盤づくりとして v 1.0.0 リリース直前のテスト&ビルドフローを固めた。
その後 本番ネット回線相当で動かす E2E が真っ赤になり、丸2日かけて緑化したので経緯と学びを残す。
1. 症状とゴール— “どこが壊れていたのか / どこまで直したいのか”
症状 | 何が起きていたか | 体感インパクト |
---|---|---|
① codeload.github.com への HEAD が 400 / 502 | 社内/ISP フィルタが HTTP HEAD メソッドをブロック。GitHub Runner はアクションのメタ情報取得で HEAD を多用するため通信が途中で落ちる。 | self-hosted Runner が “Missing download info …” と即死。ワークフロー先頭で真っ赤。 |
② actions/upload-artifact@v2 ダウンロード失敗 | ①のせいで actions/* のバイナリが取得できず、checkout は通っても upload-artifact で止まる。 | ユニットテストはパスするのにアーティファクト保存ステップだけ常に失敗し、本番 E2E まで辿り着かない。 |
③ アーティファクト upload で 400 “name invalid” | 公式 Artifact API が 2024 Q4 に刷新 → v2 SDK が古いエンドポイントに POST。返ってくる 400 を Runner が「名前が不正」と誤解釈。 | 「名前おかしい?」と首をかしげ続ける負のデバッグループが発生。 |
1-2. 目指したゴール 🎯
ゴール | 具体的な判定基準 |
---|---|
A. self-hosted Mac で “Live E2E” が緑 | ✔ GitHub Actions の Live E2E ワークフローが 0 エラーで完走 |
B. テストログを GitHub アーティファクトに保存 | ✔ logs/live-e2e.log が Actions 画面右の Artifacts に残る |
C. ジョブ失敗時だけ Slack 通知 | ✔ 失敗 run で :x: メッセージが #dev-alerts に届く/成功時は無通知 |
1-3. 成功後の世界 🌈
- Runner は1日2回(0 時 / 12 時)自動で E2E を叩く
- 任意タイミングで “Run workflow” しても即グリーン
- 障害があれば Slack へ「❌ Live E2E 失敗」とリンク付きで飛ぶ
- テスト出力はアーティファクトに 90 日保管 → 後追い解析も安心
これで “本番に近いネット環境でも 壊れたら 30 分以内に気づける” パイプラインが完成!
2. 採ったアプローチ ― “赤の山を一つずつつぶす”
2-1. ローカルプロキシで HEAD → GET 変換
企業/ISP の FW が HTTP HEAD を弾く ⇒ Runner が落ちる。
そこで Node 40 行 の簡易リバースプロキシを 127.0.0.1:8080 で常駐させ、HEAD を GET に書き換えて転送した。
// proxy.mjs(主要部だけ) import http from 'http'; import https from 'https'; import net from 'net'; const server = http.createServer((cliReq, cliRes) => { const opts = { hostname: cliReq.headers.host, port: 443, path: cliReq.url, method: cliReq.method === 'HEAD' ? 'GET' : cliReq.method, // ←★変換 headers: cliReq.headers }; https.request(opts, res => res.pipe(cliRes)).end(); }); server.on('connect', (req, cSock) => net // HTTPS CONNECT は透過 .connect(443, req.url.split(':')[0], () => { cSock.write('HTTP/1.1 200 Connection Established\r\n\r\n'); }) .pipe(cSock).pipe(net.connect(443, req.url.split(':')[0])) ); server.listen(8080);
https_proxy=http://127.0.0.1:8080 # Runner 全 HTTPS をここ経由
結果:HEAD 400/502 がゼロになり、ダウンロードフェーズ突破。
2-2. upload-artifact
を Vendor(リポジトリ内取り込み)
外向き DL を完全排除したい+ v2 は API 互換切れで 400 を吐く。
rm -rf .github/actions/upload-artifact # 旧 v2 git clone --depth 1 --branch v4 \ https://github.com/actions/upload-artifact.git \ .github/actions/upload-artifact rm -rf .github/actions/upload-artifact/.git # 素のファイルに
ワークフロー側はパス参照だけ:
- uses: ./.github/actions/upload-artifact # ← vendored v4
効果:外部依存ゼロ&400 “name invalid” が半分解消。
2-3. プロキシ経由/直通を NO_PROXY で振り分け
アーティファクト用ドメインはプロキシ不要。サブドメイン全部を外すには先頭ドットが効く。
https_proxy=http://127.0.0.1:8080 NO_PROXY=api.github.com,codeload.github.com,.actions.githubusercontent.com,127.0.0.1,localhost no_proxy=${NO_PROXY} # 大小どちらを見るライブラリもある
→ pipelines*.actions.githubusercontent.com
への POST が 200 に。
2-4. アーティファクト名を run_id で一意化
同名アーティファクトがワークフロー内でロックされると 400。
with: name: live-e2e-logs-${{ github.run_id }} # 例: live-e2e-logs-15290012345
run_id は 64bit で衝突ゼロ。これで “名前は無効” 完全消滅。
2-5. Slack 通知を Docker Action → curl 1 行 に簡素化
macOS self-hosted には Docker デーモンが無い場合が多く、rtCamp/action-slack-notify
は container null
で死ぬ。
ならば直接叩くほうが早い。
- if: failure() run: | curl -X POST -H 'Content-Type: application/json' \ -d "{\"text\":\"❌ Live E2E 失敗\nCommit: ${{ github.sha }}\"}" \ ${{ secrets.SLACK_WEBHOOK_URL }}
メリット
- 依存ゼロ・起動高速
- Webhook URL だけ変えれば Teams/Discord も同じ書式で流用可
まとめ:
1️⃣ HEAD→GET で通信遮断を回避 → 2️⃣ vendored v4 で API 互換問題を解決 → 3️⃣ NO_PROXY & run_id で残りの 400 を潰す → 4️⃣ Slack を軽量化。
これで 日次 E2E が 完全緑化 + ログ永続 + 障害アラート の三拍子そろった CI に進化した。
3. 完成したワークフロー(要旨)を分解して見る
name: Live E2E # ── タイトルそのまま “本番相当 E2E” on: schedule: # ── 毎日 2 回の定期実行 - cron: "0 0,12 * * *" workflow_dispatch: # ── 手動実行ボタンも有効 jobs: live-e2e: runs-on: self-hosted # ── Mac mini 上のセルフホスト Runner steps: # ① ソースを取る(depth=1 デフォルト) - uses: actions/checkout@v3 # ② buildx でマルチプラットフォーム対応ビルド - uses: docker/setup-buildx-action@v2 # ③ 最新コードでイメージをローカルビルド - run: docker compose build mmbot # ④ 実ネット E2E を叩いてログを保存 - run: | bash tests/e2e/realnet_checks.sh 2>&1 | tee logs/live-e2e.log env: MMBOT_MODE: mainnet # 本番モード BYBIT_APIKEY: ${{ secrets.BYBIT_APIKEY }} BYBIT_SECRET: ${{ secrets.BYBIT_SECRET }} # ⑤ ログをアーティファクトにアップロード(vendored v4) - uses: ./.github/actions/upload-artifact with: name: live-e2e-logs-${{ github.run_id }} # run ごとに一意 path: logs/live-e2e.log # ファイル 1 本だけ # ⑥ 失敗した run だけ Slack に投げる(超軽量版) - if: failure() run: | curl -X POST -H 'Content-Type: application/json' \ -d "{\"text\":\"❌ Live E2E 失敗\nCommit: ${{ github.sha }}\"}" \ ${{ secrets.SLACK_WEBHOOK_URL }}
✏️ 解説ポイント
ブロック | キモ |
---|---|
runs-on: self-hosted | ローカル Mac に置いた Runner なので、企業ネットと同じ出口 で通信テストできる。 |
docker compose build | コンテナを毎回ビルドして “動くバイナリ + 最新依存” を E2E に投入。 (今後は --cache-from で高速化予定) |
realnet_checks.sh | Bybit のメインネット WebSocket / REST を実際に叩く。 → 単体テストでは拾えない “API 変更” や “ネット遮断” を検知。 |
upload-artifact vendored v4 | API 互換切れを回避。run_id でロック衝突しない。 |
Slack 通知を curl 1 行 | Docker 行きのラッパー Action を使わず macOS でもコケない。 成功時は無通知=ノイジーレス、失敗時だけ鳴る。 |
🚦 動作フロー
- 0:00 / 12:00(または手動)で Runner が起動
- イメージをビルド → 本番 API で実 E2E →
logs/live-e2e.log
生成 - ログを GitHub UI に保存
- テスト OK → ジョブ緑 / NG → Slack に ❌ + Commit SHA
結果、「落ちたら 30 分以内に Slack」+「いつでもログ確認」 が保証される。
日次監視と CI の両立、最低限これだけでかなり安心感のある運用になった。
4. 学びメモ
気づき | Tips |
---|---|
GitHub Runner は HEAD を投げる | FW が HEAD を弾く場合は GET へ変換するだけで通る |
actions/upload-artifact v2 は実質 deprecated | v4 にすると 400 エラーが一撃解消 |
no_proxy は前にドットでワイルドカード | .actions.githubusercontent.com → すべての pipelines サブドメインを直通 |
セルフホストで Docker-action は要注意 | macOS に Docker デーモンが無いと container null エラー。curl に置き換えると早い |
プロキシ常駐は pm2 が楽 | pm2 start proxy.mjs && pm2 save で再起動耐性◎ |
5. 次の TODO
- pm2 で
proxy.mjs
常駐+自動再起動 .env
をリポジトリ wiki にサンプルとして残す- 旧ファイル削除・
git gc
でサイズ整理 ACTIONS_STEP_DEBUG=true
を Secret に追加(トラブル時のみ有効化)
これで本番相当ネットでも E2E が 100 % グリーンに!
しばらくはアラートが鳴らない平穏な日々が続く…はず。次回は Slack への成功通知と GHCR への自動 push を整えて “v1.1.0 CI 完全盤” へステップアップ予定。
ChatGPTo3による 評価:82 / 100 点

「今回完成させたスクリプトをプロbotterの目線で採点するなら100点満点で何点くらいつく?シビアに評価してみて。改善点も欲しい。」という指示を出してChatGPTo3に評価してもらいました。
観点 | 加点要素 | 減点要素 | 小計 |
---|---|---|---|
信頼性 | - プロキシ+NO_PROXY 切替でネット障害耐性 ◎ - vendored v4 で外部依存ゼロ | - proxy.js が PM2 常駐前提。落ちると全部巻き添え | 18/20 |
セキュリティ | - Secrets 参照のみで値は一切ハードコード無し | - curl -vI で User-Agent 丸出し(FW ログに残る) | 15/20 |
保守性 | - run_id で重複回避、if-no-files-found も設定 - ステップ名が具体的 | - バージョン pin が緩い (@v3 / @v2 )- Connectivity Check とテスト本体が 1 ワークフローに混在 | 14/20 |
パフォーマンス | - buildx 使用でマルチアーキ OK | - 毎回 docker compose build → キャッシュ効かず遅い | 10/15 |
可読性 | - コメント丁寧。Slack 失敗通知が分かりやすい | - 長い `curl | tee` ワンライナーは読みにくい |
ベストプラクティス | - アーティファクト名一意化 - cron & 手動両対応 | - 成功時通知が無い → “落ちた時だけ” は良いが CI Dashboard が寂しい - actions/checkout@v3 など EOL 迫る | 13/20 |
総合:82 点
「壊れないための工夫」は十分。“毎日回す本番 E2E” としては合格ラインだけど、もう一歩で満点。
改善アイデア 6 連発
番号 | 施策 | メリット |
---|---|---|
1 | proxy.mjs を PM2 常駐化し、healthcheck エンドポイントを実装 | プロキシ落ちても自動再起動+ランナー側で監視できる |
2 | Docker レイヤキャッシュを actions/cache or buildx に追加 | Build ステップが 30 → 数秒に短縮 |
3 | バージョン pin を commit hash に固定actions/checkout@v3 → @v3.6.0 | 予期せぬメジャーアップデート事故防止 |
4 | ステップ分割 or Reusable Workflow ①ネット疎通ヘルスチェック ②E2E テスト本体 | 障害切り分けが楽+再利用性アップ |
5 | 成功時も Slack reaction だけ送信:white_check_mark: をレスポンスに追加 | チーム側で「緑かどうか」を一目で把握 |
6 | concurrency: キーでジョブ競合防止concurrency: group: live-e2e cancel-in-progress: true | 前回 run がダラけても新しい run を優先できる |
結論:現状でも CI/監視としては合格。ただ「速さ・保守・次期 GitHub Actions の潮目」を意識して上記6点を磨けば、満点近くまで持っていける!
“o4 で回し → 詰まったら o3” という切り替え戦略のポイント

o4-mini-highとo3を使い分けたのが、私的には重要ポイントだった。基本はo4だけど、3回ほど試してもダメだったらo3に切り替えてピンポイントで潰す、みたいな感じで使った。抽象度を上げて引きの視点で問題解決したい時にはo3の方が良い。
使いどころ | o4-mini-high (高速・量産型) | o3 (推論重視型) |
---|---|---|
長所 | - レスポンスが軽快、試行回数を稼げる - 細かなログ確認やスクリプト添削など“手数”が欲しい作業で強い | - 文脈を俯瞰し、問題の核心を抽象化して提示してくれる - 「そもそも設計を変えた方が早い」系の示唆が得やすい |
短所 | - “目の前のログ” には強いが、大局観はやや弱い - 似た提案を繰り返す場合あり | - 応答がやや重め、細部のコピペ確認は苦手 - ステップバイステップで逐次検証したい局面では冗長に感じる |
相性が良いタスク | ・docker build エラー三段活用 ・YAML インデント修正 ・ grep -R の一発回答 | ・プロキシ設計の抜本見直し ・NO_PROXY ドメインパターン整理 ・CI フロー再構成の方針決定 |
使い分けフロー(実際にやったこと)
- まず o4 でラピッド試行
- HEAD→GET 変換案、PF DNAT、nginx proxy_method… と3連投
- ログを貼って「これでいける?」→ ダメならすぐ次案
- 3ストライクで o3 にバトンタッチ
- 質問を抽象度アップ:「通信経路を整理するとしたら、依存を3段階で分けて教えて」
- o3 が“プロキシバイパスと SDK の互換切れ”を同時に指摘 → 問題を俯瞰できた
- 核心が見えたら再び o4 で実装フェーズへ
- v4 Vendor 手順や
.env
具体例を高速に生成 - Slack curl ワンライナなどピンポイント改修
- v4 Vendor 手順や
学び
- 高速探索 × 高解像度の切り替えはコスト最小
- o4 で“量”を出して枝切り
- o3 で“芯”を射抜き
- 仕上げを o4 で量産
- 抽象レイヤを意識して質問を変える
- o4 には “このログの 400 を 200 にしたい” と具体
- o3 には “CI ネットワーク全体の設計を洗い直すと?” と俯瞰
このハイブリッド運用で、最短ルートよりむしろ “寄り道しつつも深い理解が得られる” 開発体験になった。