ノードネットワーク設計思想
このドキュメントについて
このUSBには、複数のマシンが協調してgitリポジトリを保持する仕組みが含まれています。セットアップ手順は install.sh と toki-push.sh のコメントに書いてあります。
このドキュメントはその「なぜ」を記録するものです。10年後にこのUSBを開いた方が、コマンドを写経するのではなく、設計の意図を理解して再構築できるように。
解いた問題
gitリポジトリを複数のマシンで共有したい。ただし:
- GitHubに依存しない(アカウント削除・サービス終了のリスク)
- 常時起動のサーバーを前提にしない(電気代・管理コスト)
- クラウドDBを前提にしない(コスト・依存)
- 複数マシンが同時に参加できる
- ネットワーク障害時でも物理(USB)で完結する
最終的な構成
物理レイヤー USB bare repo(オフライン完結)
デジタルレイヤー 各マシンの node.py(Tailscale Funnel で公開)
発見レイヤー Cloudflare Pages の nodes/index.txt + nodes/{id}.json
pushするとき、toki-push.sh は次の順で試みます:
- 現在の remote に push
- 失敗したら Pages から
nodes/index.txtを取得 - 各ノードの
nodes/{id}.jsonを取得して/api/healthを probe - 生きているノードに remote を切り替えて push
USBが手元にある場合は、USB内の nodes/ をオフラインフォールバックとして使います。
捨てた選択肢とその理由
Cloudflare D1(データベース)
最初、オンラインノードの一覧をD1に持つ設計を作りました。各ノードが5分ごとに「生きている」ことをD1に登録し、Workerがその一覧を返します。
捨てた理由:D1はWorkerからしか書けません。つまりWorkerへの依存が生まれます。ノード発見のためだけにWorkerとDBを維持するのは過剰でした。
WebSocket
ノードの切断をリアルタイムに検知するため、WebSocketでWorkerに接続し続ける設計を検討しました。
捨てた理由:git syncはリアルタイム性を必要としません。pushが失敗したときに別のノードへ切り替える方式で十分でした。WebSocketは常時起動のマシンか、Cloudflare Durable Objects(有料)を前提にします。どちらも依存が重くなります。
Durable Objects
WebSocketの状態を集約するためにDurable Objectsを検討しました。
捨てた理由:有料($5/月〜)です。また、状態を持つサーバーが必要という問題を解決するなら、手元のミニPCをそのまま使えます。外部サービスを使う前に「手元で同じことができないか」を先に考えます。
常時起動のレジストリマシン
ミニPCをWebSocketレジストリとして動かし、他のノードが接続するP2P構成を検討しました。
捨てた理由:「常時起動のマシンがある」を前提にすると、そのマシンが落ちたとき全体が止まります。前提をなくした設計の方が堅牢でした。
nodes/index.txt の設計変遷
ノード一覧の管理方法について3案を経ました。
第1案(失敗):nodes.json に全ノードをまとめて管理。
問題:複数マシンが同時に install.sh を実行すると、両者が同じファイルを上書きし、片方の登録が消えます。
第2案(不十分):nodes/index.txt(1行1ノードID)に各ノードが行を追記。gitが行単位でマージします。
問題:同じ「末尾」に追記する場合、gitでもconflictが発生します。
第3案(採用):nodes/index.txt はgit管理外にします。nodes/{id}.json(各ノードが自分のファイルだけ書く)をgit管理します。index.txt はデプロイ直前に ls nodes/*.json | sort で生成します。
ls nodes/*.json | xargs -I{} basename {} .json | sort > nodes/index.txt
wrangler pages deploy ...
これで:
- 書き込みの衝突がゼロになります(各ノードは自分のファイルだけ触ります)
- index.txtは生成物なので「正しさ」が保証できます
- タイミングによって一時的に1件古い可能性がありますが、次のデプロイで自己修復します
「落ちることはない。タイミングのズレは自己修復する。」これで十分でした。
エラーを隠さない
install.sh はwranglerが入っていない環境では、デプロイを自動実行しません。代わりに実行すべきコマンドを表示して終わります。
echo "note: run 'wrangler pages deploy ...' to publish nodes/"
複雑なリトライロジックを入れるより、人間に見せて判断させる方が堅牢なケースがあります。エラーを握りつぶして「成功したように見せる」実装は、後で原因がわからなくなります。
これはゼロ円主義の設計原則「シンプルを保つ」と同じ発想です。
残した依存
| サービス | 用途 | 代替可能か |
|---|---|---|
| Cloudflare Pages | nodes/ の配信 | ほかの静的ホスティングでも可 |
| Tailscale | ノード間通信 | WireGuardを自前で立てることも可 |
Cloudflare Pagesはgitリポジトリを丸ごとpushすれば動く静的ホスティングです。仮にCloudflareがサービスを終了しても、同じ構成は別の静的ホスティングで再現できます。
Tailscaleはゼロ設定でVPNを張れるサービスです。オープンソース版(Headscale)を使えばTailscale社への依存も外せます。
どちらも「なければ別のもので代替できる」依存です。「これがないと動かない」依存ではありません。
この設計が目指したもの
依存先を減らせるだけ減らすことです。
常時起動のサーバーが必要な設計は、そのサーバーが落ちたとき全体が止まります。DBが必要な設計は、DBのコストと管理が発生します。WebSocketが必要な設計は、接続を維持するインフラが必要になります。
ひとつひとつの依存に「本当に必要か」を問い続けた結果、残ったのはファイルのリストとP2Pの通信だけでした。
物理(USB)とデジタル(Tailscaleノード)の二重化は、どちらか一方が使えないときにもう一方で動くことを保証します。どちらも落ちたとき初めてgit syncが止まります。それで十分です。