OCMノード削除の重大バグ——偽削除問題
2026-02-16 | Joe's Tech Blog #035
問題の発見
今日、冷や汗が出るようなバグに遭遇した。
ユーザーがOCMのWeb画面で「ノード削除」をクリックすると、システムは「削除成功」と表示し、画面からもノードが消える。一見すべて正常に見える。しかし、削除されたはずのPC-BにSSHで接続してみると——OpenClawサービスが元気に動き続けていた。
これがいわゆる「偽削除」だ。OCMは自身のデータベース上の管理レコードを削除しただけで、ターゲットマシン上のOpenClawインスタンスにはまったく手を付けていなかった。ユーザーは完全に削除されたと思い込んでいるが、実際には何も削除されていない。これは単なる機能バグではなく、信頼に関わる問題だ——「削除済み」という前提で他の操作を行えば、さまざまな予期しない競合が発生しかねない。
問題の根本原因
コードを見直したところ、削除ロジックが行っていたのはたった一つのことだけだった:DELETE FROM nodes WHERE id = ?。このSQL一行だけで、最も基本的なリモートクリーンアップすら実装されていなかった。
正直に言えば、これは初期の高速開発時に残した技術的負債だ。当時は「とりあえず動かそう」という考えで、「あとで対応する」の「あとで」がユーザーが実際に使い始めるまで先延ばしになっていた。これは大きな教訓だ:リソースのライフサイクル管理に関わる機能は、初日から完全に実装すべきだ。
修正方針:11ステップの完全クリーンアップフロー
削除フローを再設計し、11のステップに分割して、最初から最後まで徹底的にクリーンアップするようにした:
1. OpenClawサービスの停止 — systemctl stop openclaw
2. 自動起動の無効化 — systemctl disable openclaw
3. 設定のバックアップ — 削除前に重要な設定を/tmp/openclaw-backup-{timestamp}/にバックアップ
4. Telegram Botの削除 — BotFather APIを呼び出してBotを登録解除
5. Sessionデータのクリーンアップ — すべてのagentセッションファイルを削除し、ディスク容量を解放
6. OpenClawのアンインストール — npm uninstall -g openclawまたはインストールディレクトリの削除
7. 設定ディレクトリのクリーンアップ — ~/.openclaw/を削除
8. systemdユニットファイルのクリーンアップ — /etc/systemd/system/openclaw.serviceを削除してdaemonをリロード
9. OCMレジストリの更新 — 最後にデータベースからレコードを削除
10. クリーンアップ結果の検証 — SSHで戻ってプロセス、ポート、ファイルがすべて削除されたことを確認
11. 操作ログの記録 — 完全な監査ログ
重要な設計原則:先にリモートをクリーンアップし、最後にローカルレコードを削除する。途中のいずれかのステップが失敗しても、ローカルレコードが残っているため、ユーザーはノードが「クリーンアップ中」の状態にあることを確認でき、リトライが可能だ。
セキュリティ機能
実装過程で、いくつかの重要なセキュリティメカニズムを組み込んだ:
スマートなエラーハンドリング:各ステップに独立したtry-catchを設けた。ステップ3のバックアップが失敗しても後続のクリーンアップには影響しないが、ログには明確に記録される。クリティカルなステップ(サービスの停止など)が失敗した場合は、フローを中断してユーザーに通知する。
タイムアウト保護:各SSHコマンドにタイムアウトを設定(デフォルト30秒)。以前、ターゲットマシンのネットワークが不通でSSHがハングする事態に遭遇したことがある。タイムアウトメカニズムにより無限に待ち続けることを防止する。
詳細なログ:各ステップの実行結果、所要時間、エラー情報をすべて記録。事後の問題調査で、これらのログに何度も助けられた。
async function deleteNode(nodeId) {
const steps = [
{ name: 'stop-service', critical: true, timeout: 30000 },
{ name: 'disable-autostart', critical: false, timeout: 10000 },
{ name: 'backup-config', critical: false, timeout: 60000 },
// ... 後続ステップ
];
for (const step of steps) {
try {
await executeWithTimeout(step.fn, step.timeout);
log.info(✅ ${step.name} completed);
} catch (err) {
if (step.critical) throw new DeletionError(step.name, err);
log.warn(⚠️ ${step.name} failed (non-critical): ${err.message});
}
}
}
PC-Bの手動クリーンアップ
新しいコードをデプロイする前に、まずPC-Bを手動でクリーンアップした。全体の作業は約15分かかった:
ssh user@pc-b
sudo systemctl stop openclaw
sudo systemctl disable openclaw
sudo rm /etc/systemd/system/openclaw.service
sudo systemctl daemon-reload
rm -rf ~/.openclaw
クリーンアップ完了を確認
ps aux | grep openclaw # 結果なし
ss -tlnp | grep 18789 # 結果なし
クリーンアップ完了後の気分——爽快だ。散らかった部屋を掃除し終えたような感覚。
教訓
このバグを通じて、「削除」という一見シンプルな操作の裏にある複雑さを深く理解した。分散システムにおいて、複数ノードの状態変更を伴う操作は、決して半端なままにしてはならない。
核心原則:ユーザーにとって「削除」は「完全に消える」ことを意味する。完全にできないなら、「成功」と表示すべきではない。
今後、OCMに「削除プリチェック」機能を追加する予定だ——まずターゲットノードが到達可能か、サービスの状態はどうかを確認し、ユーザーに明確なプレビューを提示する:「以下のコンテンツがクリーンアップされます:xxx」。削除を透明で、制御可能で、信頼できるものにする。