# Multi-site active failover pilot (see # /home/scooby/.claude/plans/jiggly-snacking-iverson.md) - home's half of # the failover-watcher. This is deliberately NOT a general Postgres HA # controller: its only job is writing a fresh unix timestamp to etcd every # 10s at key /ha-failover/home-heartbeat. The VPS's watcher # (scripts/ha-failover-watcher.sh, deployed via systemd - see that # script's own header) reads this key and decides whether to promote # based purely on how stale it is - no custom voting/consensus logic # needed here, etcd's own Raft consensus already provides the "majority # agrees" guarantee: a write only succeeds if a majority of the 3 etcd # members (home/vps/witness) are reachable and agree, and a linearizable # read (the JSON gateway's default) only ever returns majority-confirmed # state. # # Talks to etcd via its LOCAL ClusterIP (ha-etcd.ha-failover.svc.cluster.local # :2379) - home reads/writes its OWN cluster member directly, no need to # round-trip externally for this side. # # No RBAC/ServiceAccount needed - this pod never touches the K8s API, # only etcd's HTTP gateway via curl. apiVersion: v1 kind: ConfigMap metadata: name: ha-heartbeat-script namespace: ha-failover data: heartbeat.sh: | #!/bin/sh set -eu KEY_B64="$(printf '%s' '/ha-failover/home-heartbeat' | base64 | tr -d '\n')" while true; do NOW="$(date +%s)" VAL_B64="$(printf '%s' "$NOW" | base64 | tr -d '\n')" if curl -sf --max-time 5 -X POST \ http://ha-etcd.ha-failover.svc.cluster.local:2379/v3/kv/put \ -d "{\"key\":\"${KEY_B64}\",\"value\":\"${VAL_B64}\"}" >/dev/null; then echo "heartbeat ${NOW} ok" else echo "heartbeat ${NOW} FAILED (etcd unreachable or no quorum)" fi sleep 10 done --- apiVersion: apps/v1 kind: Deployment metadata: name: ha-heartbeat-writer namespace: ha-failover spec: replicas: 1 selector: matchLabels: app: ha-heartbeat-writer template: metadata: labels: app: ha-heartbeat-writer spec: securityContext: runAsNonRoot: true runAsUser: 1000 seccompProfile: type: RuntimeDefault containers: - name: heartbeat image: alpine/k8s:1.32.13 command: ["/bin/sh", "/scripts/heartbeat.sh"] securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] resources: requests: cpu: 5m memory: 16Mi limits: memory: 64Mi volumeMounts: - name: script mountPath: /scripts volumes: - name: script configMap: name: ha-heartbeat-script defaultMode: 0755