消せる中心を作らない
── GitベースのPi分散レジストリ

哲学から設計、仕様、実装、配布方法、導入手順、教訓まで。承認のいらないインフラの全記録。

一、哲学

2026年3月27日、GitHubアカウントが突然停止された。その朝に気づいたことがある。現代のインフラのほとんどは「承認経済」の上に成り立っている。国家が国籍を承認する。企業がアカウントを承認する。プラットフォームが存在を許可する。承認が取り消されれば、そこに積み上げてきたものごとが消える。

GitHubをやめてPiに移行したが、それだけでは「単一障害点が移動しただけ」だ。Pi1台が壊れれば、デプロイも記録も止まる。問題の本質は承認の有無ではなく、消せる中心が存在することにある。

消せる中心を作らない。誰かが一点を止めても、他は動き続ける。これが設計の根本原則だ。

自治会はその思想の体現だ。非営利で、地縁に根ざし、国家の承認なしに存在する。全国の自治会・地域コミュニティがノードになれば、誰も「閉鎖」を決定できないネットワークが生まれる。そのノード管理を、GitとRaspberry Piだけで実現する。

二、設計

全体構造はシンプルだ。各Piが「自分は生きている」という情報を定期的に書き、primaryノードがそれを統合する。Macはその情報を読んでpush先を動的に決定する。

[各Pi — 毎分]
  registry-heartbeat.sh
    → queue/{node-id}.json を書いてpush

[primaryPi — 毎分]
  registry-consolidate.sh
    → queue/*.json を読んでnodes.jsonに統合

[Mac]
  pi-push.sh {repo}
    → nodes.jsonからprimary hostを解決
    → git push toki@{host}:/repos/{repo}.git
    → post-receive hook → wrangler pages deploy → Cloudflare Pages

設計上の核心は3つある。

三、仕様

ファイル構造

registry.git/                     ← bare repo(各Pi/Macのclone先)
~/repos/registry/                  ← Pi上のworking tree
  nodes.json                       ← primaryが管理する統合ファイル
  queue/
    tokiring-node-01.json          ← 各Piが自分で書く(1Pi=1ファイル)
    tokiring-node-02.json

nodes.json スキーマ

{
  "primary": "tokiring-node-01",
  "updated_at": "2026-03-28T14:21:01Z",
  "nodes": [
    {
      "id": "tokiring-node-01",
      "host": "tokiring-node-01.local",
      "tunnel": null,
      "status": "active",
      "primary": true,
      "last_seen": "2026-03-28T14:19:16Z",
      "repos": ["lp", "qr", "tokiring", "hello-briefing", "corporate"]
    }
  ]
}

フィールド定義

フィールド説明
primarystring現在のprimary node ID
updated_atISO8601consolidate最終実行時刻。5分以上古ければ昇格トリガー
idstringノード識別子。ホスト名推奨
hoststringLAN内ホスト名またはIP
tunnelstring|nullCloudflare Tunnel等の外部URL。nullならhostを使用
statusactive|inactivelast_seenが300秒以内ならactive
reposstring[]このノードが持つbare repoの一覧

四、実装

registry-heartbeat.sh(各Pi、毎分実行)

自ノードの情報を queue/{id}.json に書いてpushする。bare repoの一覧は ~/repos/*.git から自動検出する。

#!/bin/bash
PI_ID="tokiring-node-01"
REGISTRY_DIR="/home/toki/repos/registry"

REPOS=()
for d in /home/toki/repos/*.git; do
  name=$(basename "$d" .git)
  [ "$name" = "registry" ] && continue
  REPOS+=("\"$name\"")
done
REPOS_JSON=$(IFS=,; echo "${REPOS[*]}")
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

cd "$REGISTRY_DIR"
git fetch origin --quiet
git merge --ff-only origin/main --quiet 2>/dev/null \
  || git reset --hard origin/main --quiet

cat > "queue/${PI_ID}.json" << QEOF
{ "id": "${PI_ID}", "host": "tokiring-node-01.local",
  "tunnel": null, "status": "active",
  "last_seen": "${NOW}", "repos": [${REPOS_JSON}] }
QEOF

git add "queue/${PI_ID}.json"
if ! git diff --staged --quiet; then
  git -c user.name="TokiStorage Bot" \
      -c user.email="tokistorage1000@gmail.com" \
      commit -m "heartbeat: ${PI_ID} ${NOW}"
  git push origin main
fi

registry-consolidate.sh(primaryのみ、毎分実行)

nodes.jsonのupdated_atが5分以上古ければ自分がprimaryに昇格し、queue/*.jsonを統合してnodes.jsonを更新する。

# primary判定: updated_atが5分以上古い or 自分がすでにprimary
DIFF=$(( $(date -u +%s) - UPDATED_EPOCH ))
if [ "$CURRENT_PRIMARY" != "$PI_ID" ] && [ "$DIFF" -lt 300 ]; then
  exit 0  # 現primaryが生きている → 何もしない
fi

# queue/*.json をPython3で統合 → nodes.json更新 → push
python3 - "$PI_ID" "300" "$REGISTRY_DIR" "$NOW_ISO" << 'PYEOF'
import json, os, sys
from datetime import datetime, timezone
PI_ID, STALE, REG_DIR, NOW_ISO = sys.argv[1], int(sys.argv[2]), sys.argv[3], sys.argv[4]
now = datetime.now(timezone.utc)
nodes = []
for fname in sorted(os.listdir(os.path.join(REG_DIR, 'queue'))):
  if not fname.endswith('.json'): continue
  d = json.load(open(os.path.join(REG_DIR, 'queue', fname)))
  age = (now - datetime.fromisoformat(d['last_seen'].replace('Z','+00:00'))).total_seconds()
  d['status'] = 'active' if age < STALE else 'inactive'
  d['primary'] = (d['id'] == PI_ID)
  nodes.append(d)
json.dump({"primary": PI_ID, "updated_at": NOW_ISO, "nodes": nodes},
          open(os.path.join(REG_DIR, 'nodes.json'), 'w'), ensure_ascii=False, indent=2)
PYEOF

pi-push.sh(Mac側)

registry cloneを最新化し、activeなprimaryのhostを解決してgit pushを実行する。tunnelが設定されていればtunnelを優先する。

#!/bin/bash
REPO_NAME="${1:?Usage: pi-push.sh <repo-name>}"
REGISTRY_LOCAL="$HOME/work/registry"

git -C "$REGISTRY_LOCAL" fetch origin --quiet
git -C "$REGISTRY_LOCAL" reset --hard origin/main --quiet

PRIMARY_HOST=$(python3 -c "
import json, sys
d = json.load(open('$REGISTRY_LOCAL/nodes.json'))
for n in d.get('nodes',[]):
  if n['id'] == d.get('primary') and n.get('status') == 'active':
    print(n.get('tunnel') or n['host']); sys.exit(0)
sys.exit(1)")

git push "toki@${PRIMARY_HOST}:/home/toki/repos/${REPO_NAME}.git" HEAD:main

五、成果物

2026年3月28日時点で稼働しているもの:

六、配布方法

新しいPiへの導入はワンライナーで完結する。pi/setup.sh はCloudflare Pagesから直接取得できるため、GitHubもgit cloneも不要だ。

curl -sSL https://tokistorage-tokiring.pages.dev/pi/setup.sh | bash -s -- \
  CF_ACCOUNT_ID=xxx \
  CF_D1_DATABASE_ID=xxx \
  CF_API_TOKEN=xxx \
  VAPID_SUBJECT=mailto:your@email.com \
  VAPID_PUBLIC_KEY=xxx \
  VAPID_PRIVATE_KEY=xxx

setup.shが行うこと:Node.jsのインストール確認、静的ファイルのダウンロード、daemon.jsのインストール、wranglerのインストール、.envの書き込み、crontabへの登録。

将来的には、このsetup.shがregistryへの自動登録も行う。USBに差すだけでノードがネットワークに参加し、pi-push.shが自動でそのノードを認識する。

七、導入手順

新規Pi(2台目以降)をregistryに追加する手順

# 1. setup.sh を実行(上記ワンライナー)

# 2. 既存Pi(primary)からregistryをclone
git clone toki@tokiring-node-01.local:/home/toki/repos/registry.git \
          ~/repos/registry

# 3. heartbeat/consolidate スクリプトを配置
curl -sSL https://tokistorage-tokiring.pages.dev/pi/registry-heartbeat.sh \
     -o ~/deploy/registry-heartbeat.sh
chmod +x ~/deploy/registry-heartbeat.sh

# 4. crontab に追加
( crontab -l 2>/dev/null
  echo "* * * * * bash ~/deploy/registry-heartbeat.sh >> ~/logs/registry-heartbeat.log 2>&1"
  echo "* * * * * bash ~/deploy/registry-consolidate.sh >> ~/logs/registry-consolidate.log 2>&1"
) | crontab -

# 5. 動作確認(1分後)
tail -f ~/logs/registry-heartbeat.log

Macのpush先を切り替える手順

# registryのcloneが古ければ手動更新
git -C ~/work/registry fetch origin && git -C ~/work/registry reset --hard origin/main

# 通常のpushはpi-push.shを使う
~/bin/pi-push.sh lp      # lpリポジトリをprimaryにデプロイ
~/bin/pi-push.sh tokiring

八、留意点

九、教訓と啓示

GitHubに消された日から、ここまで来るのに1日もかからなかった。プラットフォームを失った瞬間に次の構造が見えたのは、思想が先にあったからだ。インフラは思想の結晶であり、思想のないインフラは脆い。

技術的な教訓は一つに集約される。プロトコルはプラットフォームより長生きする。gitはHTTPやSSHの上で動く仕様だ。GitHubが消えてもgitは消えない。TCP/IPが存在する限り、Piはどこかのサーバーと通信できる。

自治会という組織が承認の外側にある最後の拠り所であるように、gitというプロトコルはサービスの外側にある最後のインフラだ。どちらも「消せる中心」を持たない。

消せる中心を作らない。
プロトコルはプラットフォームより長生きする。

このシステムが伝えたい啓示は一つだ。インフラを所有することは、承認を必要としないことだ。Raspberry Piは3,000円のmicroSD一枚で誰でも持てるインフラになる。それが1台でも、100台でも、設計は変わらない。消せる中心が存在しないから、誰も止められない。

承認なしで、存在できる。
それはコードの話ではなく、インフラの設計思想だ。