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

🛠️開発記録#245(2025/6/5)Slack 断で即停止! クリプトBot監視を30行で仕上げた話

2025年6月5日

1. はじめに – “防御ファースト”の一日

仮想通貨の自動取引botを実戦投入する前に、どうしても先に片付けておきたかった課題がある。
それが 「Slack通知が飛ばなくなったらBotを即座に止める仕組み」 だ。

一見すると「そこまでしなくても…」と思えるかもしれない。
けれど私にとっては、これが本番投入への “絶対条件” だった。

なぜなら、botの暴走が発覚する頃には大抵ポジションは天井か底を突き抜けていて、
Slack通知が届かないというのはすでに「異常が外部から観測できない状態」を意味するからだ。

今回はこの “一撃で止めるための監視スクリプト” を 最小の構成で仕上げる ことをテーマに据え、
不要な機能を切り捨て、テストを繰り返しながら、
実際に「Slackが死んだときにBotが確実に止まるか」を確かめた一日だった。

2. 過剰機能カットから始まった

私が最初に手を付けたのは、以前使っていた watchdog.sh の整理だった。
このスクリプトには、以下のような機能が詰め込まれていた:

  • PnLファイルの更新遅延チェック
  • ログ内のWARNING行数カウント
  • Slack通知(心拍・異常検知)
  • Dockerコンテナの自動再起動(最大3回)
  • 再起動回数の一時ファイル管理
  • .env ファイルの読み込み
  • set -euo pipefail によるエラートラップ

一見すると「よくできてるじゃん」と思えるし、
実際これで助かった場面もあった。けれど今回は、テストネットではなく実弾運用を前提とした本番環境を見据えていた。
その視点で見直してみると、「これは今じゃないな」と感じるものが多かった。

例えば、ログのWARNING数をカウントして停止する機能。
一見便利だけど、実際には“意味のないWARNING”も多く、閾値調整が面倒だった。
自動再起動も、Dockerのrestart: unless-stoppedがある以上、わざわざスクリプトでやる必要はなかった。
むしろ、失敗の連鎖でループする危険すらある。

私は思い切って、**「今必要なこと以外は全部削る」**ことにした。
そう決めたら、スクリプトは一気にシンプルになった。
最終的に残したのは、次の3つだけだった:

  1. .env から設定値を読み込む機能
  2. Slackにハートビートを送信する機能
  3. それが失敗したときに docker stop を叩く機能

ここで初めて、「今の自分に必要な安全網」だけを残すという考え方ができた気がする。

3. 30行の最小監視スクリプト

機能を削ぎ落としたことで、スクリプトは極めてシンプルになった。
いわば「守りのミニマム構成」である。

私が書いたのは、たったこれだけだ:

#!/usr/bin/env bash
set -euo pipefail

# .envから設定を読み込む
source .env.production

# Slackへハートビートを送信
resp=$(curl -m5 -sS -X POST -H 'Content-Type: application/json' \
  --data '{"text":"watchdog heartbeat"}' "$SLACK_WEBHOOK_WATCHDOG" \
  -w 'HTTP%{http_code}')
http_code="${resp##*HTTP}"
body="${resp%HTTP*}"

# 失敗条件:通信失敗、2xx以外、"ok":true が無い
if [[ "$http_code" != 2* || "$body" != *'"ok":true'* ]]; then
  echo "[SAFETY-STOP] Slack NG — stopping mmbot"
  docker stop mmbot || true
  exit 1
fi

sleep 300

構造は驚くほど単純だが、役割は明確だ。Slackに通知が届かなかったら、ただちにBotを止める。
それだけ。だが、それが重要だった。

実際にこのスクリプトが機能するかどうか、SlackのWebhookをわざと壊してテストした。
すると、5秒後に [SAFETY-STOP] Slack NG — stopping mmbot のログが流れ、
Botコンテナが停止された。狙った動作が、一切の迷いなく実行された。

コードがシンプルであることの意味を、私はこの時はっきりと理解した。
削ることで信頼性が上がる。判断を絞ることで、動作が速くなる。

この監視スクリプトが自分で“使い切れる道具”になった瞬間だった。

4. 実テストでハマったSlack 200問題

「Slackが死んだらBotを止める」──その方針で書いたスクリプトは、理屈上は正しかった。
しかし、実際にテストを始めた瞬間、予想外の落とし穴にハマった。

SlackのWebhook URLをわざと壊して、例えば存在しないパスにしてみる。
このとき私は curl --fail を使っていたので、HTTP 4xx や 5xx が返れば curl は非ゼロで終了し、
そのまま docker stop が走る、という設計だった。

ところが、Slackは壊れたWebhookに対してもHTTP200を返してくることがある。
つまり curl から見れば “成功” に見えてしまい、スクリプトは何も異常を検知しないまま sleep 300 に入ってしまうのだ。

正確には、Slackは {"ok":false, "error":"invalid_webhook"} のようなレスポンスを返す。
つまりHTTP的には成功でも、Slack的には失敗というわけだ。
これは curl --fail では拾えない。

この仕様を理解したとき、「あぁこれは、現場で起きたらシャレにならない」と直感した。
だから、スクリプトに次のロジックを追加した:

if [[ "$http_code" != 2* || "$body" != *'"ok":true'* ]]; then
  echo "[SAFETY-STOP] Slack NG — stopping mmbot"
  docker stop mmbot || true
  exit 1
fi

つまり、HTTPステータスが2xx以外、またはSlackのレスポンス本文に "ok":true が無い場合は、失敗と見なす。

この追加によって、Webhookが壊れていても docker stop が確実に走るようになった。
「Slackが生きているか」を検知するためには、**表面的な通信成功ではなく、
その中身を見て判断する必要がある。**それを思い知った。

5. コンテナ名がズレて止まらない!?

Slack通知の失敗を検知して docker stop を実行する──
ここまでの仕組みが整った時点で、私は一度 make prod-up でBotを起動し、
意図的にSlack接続を遮断して watchdog.sh を走らせてみた。

ところが。

「Botは停止しました」というログが出たのに、コンテナは停止していなかった。

docker ps を叩くと、まだ compose-mmbot-1 というコンテナが動いていた。
スクリプトでは docker stop mmbot を実行しているのだから、おかしい。

原因はすぐにわかった。Docker Composeが自動で命名したコンテナ名と、
私が止めようとしているコンテナ名がズレていた
のだ。

Docker Composeは、サービス名にプロジェクト名を加えて
compose-<サービス名>-1 のような形式でコンテナ名を自動生成する。
一方、私の watchdog.sh では BOT_CONTAINER=mmbot を止めようとしていた。

名前が違えば止まるはずもない。

この問題を解決するために、私は compose.yml に以下の一行を追加した:

container_name: mmbot

これにより、Docker Composeが生成するコンテナ名を明示的に固定した。
この一行で、スクリプトからの docker stop mmbot が正しく対象に届くようになった。

こうして、Slack通知が失敗 → watchdogが異常を検知 → mmbotが確実に止まる、
という流れがようやく一本の線でつながった。

“名前を決め打つ”ことの大切さを、ここで改めて痛感した。
自動で名前がつくことは便利だが、“止める対象”が明確に定義されていないと、
システムは止めたつもりで止まらない
という現実がある。

6. sidecar方式 vs cron方式

Slackが落ちたらBotを止める。
そのための監視スクリプトが完成したところで、次に私が悩んだのは**「このスクリプトをどうやって常時実行させるか?」**という点だった。

選択肢は大きく2つあった。

  • cron方式:ホストOSのcronに登録し、定期的にスクリプトを叩く
  • sidecar方式:Docker Composeの別サービスとしてBotと一緒に立ち上げる

どちらにも一長一短がある。
cron方式は、Docker自体が落ちていてもスクリプトが生きているため、
「dockerdごとクラッシュした」というケースにも対応できる。
また、Dockerとは別レイヤーにあるという点で、セキュリティ面での分離が効いている。

一方、sidecar方式は圧倒的に手軽だった。
Composeファイルに10数行を追加するだけで、make prod-up の中でBotと一緒に
自動的に監視スクリプトも立ち上がる。再起動時も含めて自己完結している。
私が望んでいたのは**「Botと同じ環境で、最小の手数で、確実に動くこと」**だったので、
今回のケースではsidecarを選んだ。

その判断を後押ししたのは、**「テストネットでも起動してしまって構わない」**という割り切りだった。
cron方式であれば環境によって起動を制御しやすいが、
今回は「Slack通知が飛ばないのはそうそう起こることではない」
という判断のもと、sidecarを常時起動させることを受け入れた。

結果として、Docker ComposeでBotと監視を同時に起動・停止できるという操作感は非常に心地よく、
実運用を想定した場合にも無理のない設計になったと感じている。

7. Makefile一本でテストネットと本番を回す

Botの実行環境を管理するうえで、私がこだわっていることの一つが
**「操作の単純化」**だ。

とくに、テストネットと本番の切り替えをストレスなく行えるかどうかは、
Botを素早く回して育てるうえでの鍵になる。

私のMakefileでは、以下のように testnet / mainnet それぞれに対して
起動・停止・クリーンアップのコマンドを定義している:

# テストネット起動
test-up:
	@echo "🔹 Starting TESTNET bot ..."
	@DATA_DIR=$(DATA_DIR) \
	  docker compose -f compose/compose.yml \
	  --env-file env/.env.testnet up -d --build

# 本番起動
prod-up:
	@echo "🔹 Starting MAINNET bot ..."
	@DATA_DIR=$(DATA_DIR) BOT_ENV_FILE=../env/.env.production \
	  docker compose -f compose/compose.yml up -d --build

このように書いておけば、「make test-up」だけでテスト環境が立ち上がり、
「make prod-up」でそのまま本番に切り替えられる。

.env ファイルの読み替えやマウントパスの切り替えも、Makefileの変数で統一しておけば問題ない。

このスタイルにしてから、環境切り替えに伴う人的ミスが激減した。
以前は.envファイルを直接書き換えたり、dockerコマンドを手打ちしたりしていたため、
「testnetのつもりが本番キーを使っていた」といったヒヤリとする事故もあった。

加えて、Docker Composeでsidecarを一緒に立ち上げているため、
Botとwatchdogの起動・停止もすべて一括で制御できる。

こうした仕組みを整えておくことで、
本当に注力すべきは「ロジックの強化」であるという状態を作りやすくなる。

「環境管理に迷わない」ことは、「改善に集中できる」ことでもあるのだ。

8. CIは育成ゲーム—いまはフェーズ0でOK

CI(継続的インテグレーション)と聞くと、
「テストがすべて通らなければマージできない」
という厳しいゲートのイメージを持つ人も多いだろう。
かく言う私も以前はそうだった。“CI=完成度の高いコードを保証するもの”
という先入観があった。

でも、仮想通貨Botの開発においては、この考え方がそのまま通用するとは限らない。

Botのロジックは実験的で、日々チューニングが続く。
そのたびに「勝率が不安定」「学習中」「リファクタ途中」みたいなコードが頻繁に生まれる。
それらすべてに対してガチガチのテストを要求していたら、
コードは書けてもマージできない地獄に陥る。

そこで私は考え方を変えた。
**「CIも育てていけばいい」**のだ。

今の私が採用しているのは、“フェーズ0”の軽量CI
これが守っているのは、次のたった2つだけ:

  1. 構文エラーがないこと(py_compile
  2. 設定が破綻していないこと(--dry-run
- run: python -m py_compile $(git ls-files '*.py')
- run: python -m mmbot.cli --dry-run --env testnet

この2つさえ通れば、とりあえず動くものが回る
そしてそれが、開発を進めるうえでの最優先だ。

将来的には、勝率やPnLの閾値をCIに組み込んでいくこともできる。
でもそれは、ロジックが「ある程度勝てる」段階に到達してからでいい。
いまは、“落ちない”ことに集中する。

CIを育成ゲームと考えれば、いまはフェーズ0、必要に応じてフェーズ1、2と
段階的に進化させていく
という自然な流れが生まれる。

「最初から全部入れる」のではなく、
「必要なものから少しずつ足していく」。
これはロジックもCIも、育てるという意味では同じなのだ。

9. 今日得た3つの学び

今回の開発を通じて、私は単にコードを動かしただけではなく、
Botterとしての開発姿勢そのものをアップデートできた気がしている。
振り返ってみて、今日とくに大きかった学びを3つ挙げたい。


① 「主導権は自分にある」という実感

これまでは、AIに提案された設計や手順を“正解”としてそのまま受け入れていた。
もちろんそれは、ある種の安心感にもつながっていたのだけれど、
気づけば自分で判断しているようで、実は受け身だったという場面も少なくなかった。

今回、Slack監視の実装において「ここまでは要らない」と機能を削ったり、
CIに対して「今はこのレベルでいい」と基準を下げたり、
自分の判断で線を引いたことで、初めて開発の主導権が自分に戻ってきたと感じた。


② 最小構成で動かしてから強化する

完璧なCI、フルカバレッジのテスト、理想的な構成──
そういう“完成形”を目指しすぎると、かえって動かすまでが遠くなる。

今回は「まずはSlack通知が飛ばなかったら止める」
それだけに絞って設計し、最小の構成で走らせた。
その上で、SlackがHTTP200を返す罠に気づき、
"ok":true の確認を後付けするなど、必要な機能は後から足せるという実感を得られた。

動かしながら強化するほうが、結果的に速いし、気持ちが折れにくい。


③ わからないことは“あとで調べる”でいい

開発中、知らない用語や技術に出会うのは避けられない。
以前の私は、そうしたものに出くわすたびに深掘りしすぎて、
本来のタスクから外れてしまうことが多かった。

でも今は、わからないことはノートに書き出して後で調べる、というスタイルに変えた。
その結果、開発のテンポが格段に良くなったし、
「最低限ここまではやる」という判断もつけやすくなった。


この3つは、今後Bot開発を本格的に加速させていくうえでの
**自分なりの「開発哲学」**の土台になっていく気がしている。

10. まとめ – 小さく守って大きく攻める

✅ まずやるべきタスク

  • .env / configの整理(A2/A3)
    .env.production に含まれる各種パラメータ(LOTサイズ、損失上限など)を精査し、
    Git管理外かつSecretsストアへの登録を完了させる。
  • safe_exitの本番テスト(B1)
    実際に本番APIキーでBotを起動し、SIGINTで手動停止。
    ポジションが確実に解消され、Slack通知が飛ぶことを確認する。
  • ログとDBのローテーション設定(B2)
    長時間稼働を想定し、ログサイズとDBファイルを定期ローテート。
    logururotationretention.env化する。

📌 中期的に考えている展望

  • CIの再構築
    今は“フェーズ0”の構文チェック&dry-runだけ。
    フェーズ1(config整合性)→ フェーズ2(簡易PnLチェック)へ段階的に育てていく。
  • 実運用ログからのロジック強化
    勝率/DD/エントリースプレッドなどの評価をもとに、
    Botを“継続して勝てる状態”に進化させていく。

Botを本番に投入する。
それは、単にコードを動かすことではない。
お金を預けて、自分の判断を信じる行為そのものだ。

だから私はまず、「止める仕組み」から整えた。
Slackが落ちたらBotも止まる。それだけの仕組みを、
30行で、確実に、シンプルに動かす。

作業そのものは地味かもしれない。
でも、それによって**“壊れても死なない”土台ができたことの意味は大きい。**


この開発を通じて私は、技術以上に
自分で判断する力、線を引く力、
動かしてから考えるという柔軟性
を学んだ。

完璧を目指して動けなくなるより、
最低限のラインでまず動かして、そこから強化していく。

Slack監視という一つの仕組みを通して、
私は今後のBot開発における「守りの戦略」を獲得した気がしている。

小さく守ることができるから、大きく攻められる。
これから始まる本番フェーズに向けて、
私は今、そのスタートラインに立っている。

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