6d3a0eb — und wartet auf eine Voraussetzung, die sich am Ende mit der Headscale-HA-Arbeit trifft.Objektspeicher ist im Lab unspektakulär, aber überall: Restic-/VolSync-Backup-Targets, Artefakte, kleine statische Sites. Lange lieferte das Ceph RGW — pro Cluster. Genau das ist das Problem: Fällt der Standort aus, ist der Bucket weg. Seit die ClusterMesh-Episode eine Föderation über das Tailnet etabliert hat, wird ein echter Multi-Site-Store möglich. Die Antwort heißt Garage .
Warum nicht einfach Ceph?
Ceph RGW ist mächtig, aber schwer und im Lab-Setup single-site: Die Pools liegen in einem Ceph-Cluster. Garage ist das Gegenteil — ein einzelnes statisches Binary (FROM scratch-Image, kein Operator), gebaut für Multi-Site-Self-Hosting mit zonen-bewusster Replikation. Das Lab hat das RGW bereits stillgelegt und durch Garage ersetzt; ceph-block und ceph-filesystem bleiben (Garage legt seine eigenen PVCs darauf ab — dazu gleich).
Phase 1: drei Knoten, ein Worker-Verlust
Was heute läuft, ist ein schlichtes StatefulSet (dxflrs/garage), drei Replicas, per topologySpreadConstraints eine pro hydra-Worker verteilt. Die Konfiguration ist klein genug, um sie ganz zu zeigen:
# garage-config (garage.toml)
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "lmdb"
metadata_auto_snapshot_interval = "6h"
# 3 Knoten, einer pro Worker (zone = hostname). Quorum 2/3 → übersteht einen Worker.
replication_factor = 3
consistency_mode = "consistent"
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "__RPC_PUBLIC_ADDR__"
bootstrap_peers = [
"garage-0.garage-peer.garage.svc.cluster.local:3901",
"garage-1.garage-peer.garage.svc.cluster.local:3901",
"garage-2.garage-peer.garage.svc.cluster.local:3901",
]
Ein paar Details, die mir gefallen:
- Konsistenz, nicht „eventual". Der Lab-Mode ist
consistentmit Quorum 2/3 — Read-after-write ist garantiert, solange zwei der drei Knoten stehen. - Stabile Reconnect-Adressen. Pod-IPs wechseln beim Restart; ein initContainer ersetzt
__RPC_PUBLIC_ADDR__durch den stabilen StatefulSet-DNS-Namen des jeweiligen Pods (das Image hat keine Shell). Intra-Cluster-RPC läuft über den headless Servicegarage-peer. - Zwei PVCs auf
ceph-block:meta(lmdb-Metadaten, schnell) unddata(Blocks).
Die Ports verteilen sich klar:
| Port | Rolle |
|---|---|
3900 | S3 API |
3901 | RPC (Knoten-zu-Knoten — der Föderationspfad in Phase 2) |
3902 | S3-Web |
3903 | Admin API + /metrics |
Ein Knoten erzeugt seine Identität erst beim ersten Start — das Layout lässt sich deshalb ohne Operator nicht deklarativ setzen. Bootstrap ist daher bewusst imperativ, ein Task:
task garage:bootstrap # weist zone = pod-name zu, CAPACITY=100G default
task garage:status # 3 Knoten "connected", Layout-Version N, factor 3
Benutzen
Buckets und Keys ebenfalls per Task; S3 hängt im Tailnet an s3.${TAILNET_DOMAIN} (path-style, über shared-gateway-tailnet):
task garage:bucket-create NAME=backups
task garage:key-create NAME=backups-key # Access Key ID + Secret, einmalig
task garage:grant KEY=backups-key NAME=backups
aws --endpoint-url https://s3.${TAILNET_DOMAIN} --region garage \
s3 cp ./file.txt s3://backups/file.txt
Damit ist Garage ein vollwertiges S3-Target — z. B. für VolSync/Restic-Backups — und übersteht schon heute den Verlust eines Workers.
Phase 2: ein Knoten pro Cluster wird eine Zone
Der eigentliche Reiz kommt mit dem zweiten Cluster. Aus „zone = Worker" wird zone = Cluster: Replication-Factor 3 über drei Zonen, sodass der Verlust eines ganzen Clusters die Objekte verfügbar lässt (Quorum 2/3). Der Knoten-zu-Knoten-RPC (:3901) muss dafür über die Föderation laufen.
Der Transport ist bewusst gateway-routed, nicht über einen Node-Mesh — konsistent mit der Erkenntnis aus der ClusterMesh-Episode, dass die Föderation nord-süd über die Gateways laufen soll, nicht über ein Knoten-Mesh. Garage-Knoten bekommen tailnet-LB-IPs aus dem /28-Pool, den das tailnet-gateway annonciert; cross-cluster-RPC routet dann: Pod → lokales Gateway → Tailnet → Peer-Gateway → Peer-LB-IP.
Die eine Voraussetzung
Damit das funktioniert, muss das tailnet-gateway erst zu einem cross-cluster L4-Router werden — also in-cluster-Pod-Traffic an die tailnet-LB-CIDR des Peers weiterleiten. Das ist exakt dasselbe „future L4"-Item, auf das auch das Headscale-Endpoint-Failover wartet: ein gemeinsamer Gateway-zu-Gateway-Pfad, den mehrere Roadmap-Punkte teilen. Dazu wandert GARAGE_RPC_SECRET von den hydra-Secrets in common-secrets — es muss auf jedem Cluster identisch sein.
rpc_secret. Cross-cluster bedeutet also: dieses Secret ist der Schlüssel zum gesamten Speicher-Cluster über beide Standorte. Es gehört in SOPS/common-secrets und nirgends sonst hin.Die offene Entscheidung
Zwei Cluster sind nur zwei Zonen — und das hat einen Haken: Für einen sauberen 3-way-Faktor mit Zonen-Redundanz fehlt die dritte Zone als Quorum-Tiebreaker. Also entweder 2-way-Replikation (ohne Tiebreak) als Übergang, oder auf einen dritten Cluster warten. Und: Ist das (heute single-node) cosmos eine echte Replica-Zone — oder zunächst nur ein Witness? Das ist die eine Frage, die das Design noch offen lässt; alles andere ist verdrahtet.
Der Bogen gefällt mir, weil er sich selbst erklärt: Phase 1 übersteht heute den Verlust eines Workers, Phase 2 den Verlust eines ganzen Standorts — und beides reitet auf derselben Föderation. Der Blocker ist kein Garage-Problem, sondern derselbe L4-Router, den auch Headscale-HA und die Föderation insgesamt brauchen. Wer den baut, schaltet mehrere Roadmap-Punkte auf einmal frei.
