From c364d46ac67da6017d12eeada0fac5bd7bd24681 Mon Sep 17 00:00:00 2001 From: chemavx Date: Wed, 15 Jul 2026 17:24:02 +0000 Subject: [PATCH] feat(backup-system): segundo offsite Backblaze B2 (crown jewels) - b2-crown-jewels.sh: sync diario 04:00 de las 2 copias mas recientes de cada servicio irremplazable (todo menos uptime-kuma) a b2:chemavx-k3s-backups/crown-jewels/ (~2GB, tier gratuito). - reddit-intel.sh sube tambien a B2 (retencion 3d). - verify.sh comprueba Mega (completo) + B2 (crown jewels). - Config via Secret out-of-band rclone-conf-b2 (mega+b2, kubectl, NO git); consolidar en Infisical RCLONE_CONF cuando se anada [b2] por UI (la identity del operator es read-only por API). Co-Authored-By: Claude Fable 5 --- backup-system/RESTORE.md | 5 +- backup-system/configmap-backup-scripts.yaml | 52 ++++++++++++++++- backup-system/cronjob-backup-verify.yaml | 3 +- backup-system/cronjob-rclone-b2.yaml | 62 +++++++++++++++++++++ backup-system/cronjob-reddit-intel.yaml | 3 +- 5 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 backup-system/cronjob-rclone-b2.yaml diff --git a/backup-system/RESTORE.md b/backup-system/RESTORE.md index e715090..fcbc77f 100644 --- a/backup-system/RESTORE.md +++ b/backup-system/RESTORE.md @@ -7,8 +7,9 @@ - **Mega** (`mega:k3s-backups/`): espejo diario de `/data/backups/backups` del master (retención 7 por servicio; 3 para `uptime-kuma` y `home-master`), más el subdir `reddit-intel/` que sube el CronJob de n97. -- **Backblaze B2** (`b2:.../`): copia reducida "crown jewels" (últimas 2 de cada - servicio irremplazable). Segundo proveedor por si Mega falla. +- **Backblaze B2** (`b2:chemavx-k3s-backups/crown-jewels/` + `reddit-intel/`): + copia reducida "crown jewels" (últimas 2 de cada servicio irremplazable, todo + menos uptime-kuma). Segundo proveedor por si Mega falla. - Acceso: credenciales rclone en Infisical (`/backup-system`, key `RCLONE_CONF`). **Si el cluster está muerto, Infisical también** → usa el login de Mega/B2 de la cuenta personal (Vaultwarden... que también está en el backup: guarda estas dos diff --git a/backup-system/configmap-backup-scripts.yaml b/backup-system/configmap-backup-scripts.yaml index 1cdddac..1a9d221 100644 --- a/backup-system/configmap-backup-scripts.yaml +++ b/backup-system/configmap-backup-scripts.yaml @@ -247,6 +247,33 @@ data: log "Sync completado." log "Archivos en Mega:" rclone ls mega:k3s-backups/ --config /rclone/rclone.conf + b2-crown-jewels.sh: | + #!/bin/sh + # Segunda copia offsite (Backblaze B2): "crown jewels" — las 2 copias más + # recientes de cada servicio irremplazable (~2GB, cabe en los 10GB gratis). + # Todo menos uptime-kuma (622M de historial de heartbeats, prescindible). + # Imagen BusyBox — sin GNU-ismos. Config: Secret out-of-band rclone-conf-b2 + # (mega+b2; consolidar en Infisical RCLONE_CONF cuando se añada [b2] en la UI). + set -e + + log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; } + BACKUP_DIR=/data/backups/backups + CROWN="n8n openclaw homarr authentik-pg grafana k3s-db gitea ghost-en-db ghost-en-content zona-exclusion-db zona-exclusion-content researchowl roswell-pg umami-pg infisical-pg trilium vaultwarden filebrowser-db home-master" + + KEEP=/tmp/crown-jewels + rm -rf "$KEEP"; mkdir -p "$KEEP" + for p in $CROWN; do + for f in $(ls -1 "$BACKUP_DIR/${p}"_* 2>/dev/null | sort | tail -2); do + ln -s "$f" "$KEEP/$(basename "$f")" + done + done + N=$(ls -1 "$KEEP" | wc -l) + log "Sync de $N artefactos crown-jewels a B2..." + # -L: resuelve los symlinks; sync = espejo → la retención remota (2/servicio) es automática + rclone sync "$KEEP" b2:chemavx-k3s-backups/crown-jewels/ -L \ + --config /rclone/rclone.conf --log-level INFO --stats 60s + rm -rf "$KEEP" + log "B2 crown-jewels completado ($N artefactos)." reddit-intel.sh: | #!/bin/sh # Corre en chemavx-n97 (imagen rclone, BusyBox — sin GNU-ismos). @@ -265,6 +292,9 @@ data: rclone copy "$F" mega:k3s-backups/reddit-intel/ --config /rclone/rclone.conf --log-level INFO log "Podando copias remotas de más de 8 días..." rclone delete mega:k3s-backups/reddit-intel/ --min-age 8d --config /rclone/rclone.conf + log "Subiendo a B2 (crown jewels, retención 3 días)..." + rclone copy "$F" b2:chemavx-k3s-backups/reddit-intel/ --config /rclone/rclone.conf --log-level INFO + rclone delete b2:chemavx-k3s-backups/reddit-intel/ --min-age 3d --config /rclone/rclone.conf rm -f "$F" log "reddit-intel OK" verify.sh: | @@ -325,12 +355,32 @@ data: TOTAL=$(echo "$FILES" | grep -c "tar.gz\|sql.gz\|db.gz" || true) log "Total archivos en Mega: $TOTAL" + # === Segundo offsite: B2 crown-jewels (todo menos uptime-kuma) === + log "=== Verificación de crown-jewels en B2 ===" + CROWN="n8n openclaw homarr authentik-pg grafana k3s-db gitea ghost-en-db ghost-en-content zona-exclusion-db zona-exclusion-content researchowl roswell-pg umami-pg infisical-pg trilium vaultwarden filebrowser-db home-master" + B2FILES=$(rclone ls b2:chemavx-k3s-backups/ --config /rclone/rclone.conf 2>&1) + for svc in $CROWN; do + FOUND=0 + for d in $ACCEPTED_DATES; do + if echo "$B2FILES" | grep -q "${svc}_${d}"; then + FOUND=1 + break + fi + done + if [ "$FOUND" -eq 1 ]; then + log "[OK] b2/$svc" + else + log "[WARN] b2/$svc — sin backup en los últimos 3 días" + ERRORS=$((ERRORS + 1)) + fi + done + if [ "$ERRORS" -gt 0 ]; then log "RESULTADO: $ERRORS servicio(s) sin backup reciente. Revisar." exit 1 fi - log "RESULTADO: Todos los backups verificados correctamente." + log "RESULTADO: Todos los backups verificados correctamente (Mega + B2)." kind: ConfigMap metadata: name: backup-scripts diff --git a/backup-system/cronjob-backup-verify.yaml b/backup-system/cronjob-backup-verify.yaml index a88e15e..668855a 100644 --- a/backup-system/cronjob-backup-verify.yaml +++ b/backup-system/cronjob-backup-verify.yaml @@ -36,9 +36,10 @@ spec: - mountPath: /scripts name: scripts volumes: + # rclone-conf-b2: secret out-of-band con mega+b2 (verify comprueba ambos) - name: rclone-conf secret: - secretName: rclone-conf-infisical + secretName: rclone-conf-b2 items: - key: RCLONE_CONF path: rclone.conf diff --git a/backup-system/cronjob-rclone-b2.yaml b/backup-system/cronjob-rclone-b2.yaml new file mode 100644 index 0000000..97e3533 --- /dev/null +++ b/backup-system/cronjob-rclone-b2.yaml @@ -0,0 +1,62 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: rclone-b2-backup + namespace: backup-system +spec: + # Segunda copia offsite (crown jewels → Backblaze B2), tras el sync a Mega + schedule: "0 4 * * *" + concurrencyPolicy: Forbid + failedJobsHistoryLimit: 2 + successfulJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 1 + activeDeadlineSeconds: 3600 + template: + spec: + nodeSelector: + kubernetes.io/hostname: chemavx-k8 + restartPolicy: Never + securityContext: + runAsUser: 0 + containers: + - name: rclone + # OJO: imagen BusyBox — el script debe evitar GNU-ismos + image: docker.io/rclone/rclone:1.74.3 + command: ["/bin/sh", "/scripts/b2-crown-jewels.sh"] + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + volumeMounts: + - mountPath: /data/backups/backups + name: backup-dir + readOnly: true + - mountPath: /rclone + name: rclone-conf + - mountPath: /scripts + name: scripts + volumes: + - name: backup-dir + hostPath: + path: /data/backups/backups + type: Directory + # Secret OUT-OF-BAND (kubectl, no en git): remotes mega + b2. + # Fuente de verdad deseada: Infisical /backup-system RCLONE_CONF — + # cuando se añada el bloque [b2] en la UI, volver a rclone-conf-infisical + # y borrar este secret (la identity del operator es read-only, no pudo + # escribirse por API el 2026-07-15). + - name: rclone-conf + secret: + secretName: rclone-conf-b2 + items: + - key: RCLONE_CONF + path: rclone.conf + - name: scripts + configMap: + name: backup-scripts + defaultMode: 0755 diff --git a/backup-system/cronjob-reddit-intel.yaml b/backup-system/cronjob-reddit-intel.yaml index cbe8965..99e8fb2 100644 --- a/backup-system/cronjob-reddit-intel.yaml +++ b/backup-system/cronjob-reddit-intel.yaml @@ -46,9 +46,10 @@ spec: hostPath: path: /opt/reddit-intel type: Directory + # rclone-conf-b2: secret out-of-band con mega+b2 (sube a los dos) - name: rclone-conf secret: - secretName: rclone-conf-infisical + secretName: rclone-conf-b2 items: - key: RCLONE_CONF path: rclone.conf