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

🛠️開発記録#238(2025/5/28)— “Live E2E が緑になるまで”

今回は「セルフホスト Runner × プロキシ地獄を突破して CI を安定稼働させた話」です。

前回 #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-artifactVendor(リポジトリ内取り込み)

外向き 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-notifycontainer 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.shBybit のメインネット WebSocket / REST を実際に叩く。
→ 単体テストでは拾えない “API 変更” や “ネット遮断” を検知。
upload-artifact vendored v4API 互換切れを回避。run_id でロック衝突しない。
Slack 通知を curl 1 行Docker 行きのラッパー Action を使わず macOS でもコケない。
成功時は無通知=ノイジーレス、失敗時だけ鳴る。

🚦 動作フロー

  1. 0:00 / 12:00(または手動)で Runner が起動
  2. イメージをビルド → 本番 API で実 E2E → logs/live-e2e.log 生成
  3. ログを GitHub UI に保存
  4. テスト OK → ジョブ緑 / NG → Slack に ❌ + Commit SHA

結果、「落ちたら 30 分以内に Slack」+「いつでもログ確認」 が保証される。
日次監視と CI の両立、最低限これだけでかなり安心感のある運用になった。

4. 学びメモ

気づきTips
GitHub Runner は HEAD を投げるFW が HEAD を弾く場合は GET へ変換するだけで通る
actions/upload-artifact v2 は実質 deprecatedv4 にすると 400 エラーが一撃解消
no_proxy は前にドットでワイルドカード.actions.githubusercontent.com → すべての pipelines サブドメインを直通
セルフホストで Docker-action は要注意macOS に Docker デーモンが無いと container null エラー。curl に置き換えると早い
プロキシ常駐は pm2 が楽pm2 start proxy.mjs && pm2 save で再起動耐性◎

5. 次の TODO

  1. pm2 で proxy.mjs 常駐+自動再起動
  2. .env をリポジトリ wiki にサンプルとして残す
  3. 旧ファイル削除・git gc でサイズ整理
  4. ACTIONS_STEP_DEBUG=true を Secret に追加(トラブル時のみ有効化)

これで本番相当ネットでも E2E が 100 % グリーンに!
しばらくはアラートが鳴らない平穏な日々が続く…はず。次回は Slack への成功通知と GHCR への自動 push を整えて “v1.1.0 CI 完全盤” へステップアップ予定。

ChatGPTo3による 評価:82 / 100 点

Yodaka

「今回完成させたスクリプトをプロ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 失敗通知が分かりやすい- 長い `curltee` ワンライナーは読みにくい
ベストプラクティス- アーティファクト名一意化
- cron & 手動両対応
- 成功時通知が無い → “落ちた時だけ” は良いが CI Dashboard が寂しい
- actions/checkout@v3 など EOL 迫る
13/20

総合:82 点
「壊れないための工夫」は十分。“毎日回す本番 E2E” としては合格ラインだけど、もう一歩で満点。


改善アイデア 6 連発

番号施策メリット
1proxy.mjs を PM2 常駐化し、healthcheck エンドポイントを実装プロキシ落ちても自動再起動+ランナー側で監視できる
2Docker レイヤキャッシュ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: をレスポンスに追加
チーム側で「緑かどうか」を一目で把握
6concurrency: キーでジョブ競合防止
concurrency: group: live-e2e cancel-in-progress: true
前回 run がダラけても新しい run を優先できる

結論:現状でも CI/監視としては合格。ただ「速さ・保守・次期 GitHub Actions の潮目」を意識して上記6点を磨けば、満点近くまで持っていける!

“o4 で回し → 詰まったら o3” という切り替え戦略のポイント

Yodaka

o4-mini-highとo3を使い分けたのが、私的には重要ポイントだった。基本はo4だけど、3回ほど試してもダメだったらo3に切り替えてピンポイントで潰す、みたいな感じで使った。抽象度を上げて引きの視点で問題解決したい時にはo3の方が良い。

使いどころo4-mini-high (高速・量産型)o3 (推論重視型)
長所- レスポンスが軽快、試行回数を稼げる
- 細かなログ確認やスクリプト添削など“手数”が欲しい作業で強い
- 文脈を俯瞰し、問題の核心を抽象化して提示してくれる
- 「そもそも設計を変えた方が早い」系の示唆が得やすい
短所- “目の前のログ” には強いが、大局観はやや弱い
- 似た提案を繰り返す場合あり
- 応答がやや重め、細部のコピペ確認は苦手
- ステップバイステップで逐次検証したい局面では冗長に感じる
相性が良いタスク・docker build エラー三段活用
・YAML インデント修正
grep -R の一発回答
・プロキシ設計の抜本見直し
・NO_PROXY ドメインパターン整理
・CI フロー再構成の方針決定

使い分けフロー(実際にやったこと)

  1. まず o4 でラピッド試行
    • HEAD→GET 変換案、PF DNAT、nginx proxy_method… と3連投
    • ログを貼って「これでいける?」→ ダメならすぐ次案
  2. 3ストライクで o3 にバトンタッチ
    • 質問を抽象度アップ:「通信経路を整理するとしたら、依存を3段階で分けて教えて」
    • o3 が“プロキシバイパスと SDK の互換切れ”を同時に指摘 → 問題を俯瞰できた
  3. 核心が見えたら再び o4 で実装フェーズへ
    • v4 Vendor 手順や .env 具体例を高速に生成
    • Slack curl ワンライナなどピンポイント改修

学び

  • 高速探索 × 高解像度の切り替えはコスト最小
    1. o4 で“量”を出して枝切り
    2. o3 で“芯”を射抜き
    3. 仕上げを o4 で量産
  • 抽象レイヤを意識して質問を変える
    • o4 には “このログの 400 を 200 にしたい” と具体
    • o3 には “CI ネットワーク全体の設計を洗い直すと?” と俯瞰

このハイブリッド運用で、最短ルートよりむしろ “寄り道しつつも深い理解が得られる” 開発体験になった。

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