feat(backup-system): migración completa a GitOps

- CronJob rclone-mega-backup entra en git (solo existía imperativo)
- Scripts backup.sh/verify.sh/rclone-mega.sh como ConfigMap backup-scripts
  (antes hostPath /data/backups/scripts — cambios ahora pasan por git+ArgoCD)
- rclone.conf vía InfisicalStaticSecret (homelab/prod//backup-system → RCLONE_CONF),
  sustituye al hostPath de ~/.config/rclone/rclone.conf
- Imágenes pinneadas: rclone 1.74.3, bitnami/kubectl por digest (v1.36.2)
- ClusterRole/CRB de backup-sa entran en git
- Application ArgoCD backup-system (auto-sync + prune + selfHeal)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 09:33:35 +00:00
co-authored by Claude Fable 5
parent ae0b5f9492
commit d1d2fac287
8 changed files with 286 additions and 12 deletions
+23
View File
@@ -0,0 +1,23 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: backup-system
namespace: argocd
annotations:
notifications.argoproj.io/subscribe.on-sync-failed.telegram: "5138407666"
notifications.argoproj.io/subscribe.on-health-degraded.telegram: "5138407666"
spec:
project: default
source:
repoURL: https://git.chemavx.xyz/chemavx/k8s-manifests
targetRevision: HEAD
path: backup-system
destination:
server: https://kubernetes.default.svc
namespace: backup-system
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
+15
View File
@@ -0,0 +1,15 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: backup-role
rules:
- apiGroups: [""]
resources:
- pods
- pods/exec
- persistentvolumeclaims
- persistentvolumes
verbs:
- get
- list
- create
@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: backup-role-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: backup-role
subjects:
- kind: ServiceAccount
name: backup-sa
namespace: backup-system
+143
View File
@@ -0,0 +1,143 @@
apiVersion: v1
data:
backup.sh: |
#!/bin/bash
set -e
BACKUP_DIR="/data/backups/backups"
DATE=$(date +%Y-%m-%d_%H-%M-%S)
KUBECONFIG=/etc/rancher/k3s/k3s.yaml
mkdir -p "$BACKUP_DIR"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
# === n8n ===
log "Backing up n8n..."
tar -czf "$BACKUP_DIR/n8n_${DATE}.tar.gz" -C /data/n8n . 2>/dev/null && log "n8n OK"
# === openclaw ===
log "Backing up openclaw..."
tar -czf "$BACKUP_DIR/openclaw_${DATE}.tar.gz" -C /data/openclaw . 2>/dev/null && log "openclaw OK"
# === homarr ===
log "Backing up homarr..."
HOMARR_DB_PV=$(kubectl get pvc -n homarr homarr-db-pvc -o jsonpath='{.spec.volumeName}' 2>/dev/null)
HOMARR_REDIS_PV=$(kubectl get pvc -n homarr homarr-redis-pvc -o jsonpath='{.spec.volumeName}' 2>/dev/null)
mkdir -p /tmp/backup-homarr/db /tmp/backup-homarr/redis
cp /var/lib/rancher/k3s/storage/${HOMARR_DB_PV}_homarr_homarr-db-pvc/db.sqlite /tmp/backup-homarr/db/ 2>/dev/null || true
cp /var/lib/rancher/k3s/storage/${HOMARR_REDIS_PV}_homarr_homarr-redis-pvc/dump.rdb /tmp/backup-homarr/redis/ 2>/dev/null || true
tar -czf "$BACKUP_DIR/homarr_${DATE}.tar.gz" -C /tmp/backup-homarr . 2>/dev/null && log "homarr OK"
rm -rf /tmp/backup-homarr
# === authentik PostgreSQL ===
log "Backing up authentik postgres..."
kubectl exec -n authentik postgresql-0 -- pg_dump -U authentik authentik 2>/dev/null | \
gzip > "$BACKUP_DIR/authentik-pg_${DATE}.sql.gz" && log "authentik-pg OK"
# === polymarket PostgreSQL === (retirado 2026-07-06, decomisión del bot)
# postgres escalado a 0; dump definitivo verificado en
# $BACKUP_DIR/polymarket-decommission/ (+ último nocturno .last-nightly).
# === grafana ===
log "Backing up grafana..."
GRAFANA_PV=$(kubectl get pvc -n monitoring kube-prometheus-stack-grafana -o jsonpath='{.spec.volumeName}' 2>/dev/null)
mkdir -p /tmp/backup-grafana
cp /var/lib/rancher/k3s/storage/${GRAFANA_PV}_monitoring_kube-prometheus-stack-grafana/grafana.db /tmp/backup-grafana/ 2>/dev/null || true
tar -czf "$BACKUP_DIR/grafana_${DATE}.tar.gz" -C /tmp/backup-grafana . 2>/dev/null && log "grafana OK"
rm -rf /tmp/backup-grafana
# === k3s SQLite ===
log "Backing up k3s SQLite..."
tar -czf "$BACKUP_DIR/k3s-db_${DATE}.tar.gz" \
/data/k3s-db/state.db \
/data/k3s-db/state.db-shm \
/data/k3s-db/state.db-wal 2>/dev/null \
&& log "k3s-db OK" \
|| log "k3s-db WARNING: backup parcial (WAL/SHM pueden estar ausentes si DB está idle)"
# === Cleanup old backups (keep last 7 per service) ===
log "Cleaning old backups..."
for prefix in n8n openclaw homarr authentik-pg grafana k3s-db; do
find "$BACKUP_DIR" \( -name "${prefix}_*.tar.gz" -o -name "${prefix}_*.sql.gz" \) | \
sort | head -n -7 | \
xargs -r rm -f
done
log "Backup complete!"
rclone-mega.sh: |
#!/bin/bash
set -e
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
log "Iniciando sync a Mega..."
log "Origen: /data/backups/backups"
log "Destino: mega:k3s-backups/"
rclone sync /data/backups/backups mega:k3s-backups/ \
--config /rclone/rclone.conf \
--log-level INFO \
--stats 30s
log "Sync completado."
log "Archivos en Mega:"
rclone ls mega:k3s-backups/ --config /rclone/rclone.conf
verify.sh: |
#!/bin/sh
set -e
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
# polymarket-pg retirado el 2026-07-06 (decomisión del bot)
EXPECTED_SERVICES="n8n openclaw homarr authentik-pg grafana k3s-db"
ERRORS=0
# Ventana de frescura: el sync a Mega es semanal (domingos 03:00), así que
# un backup es "reciente" si su fecha está en los últimos 8 días (0..7 atrás).
# BusyBox date no soporta -d 'yesterday'; se calcula con epoch (-d @segundos).
NOW=$(date +%s)
ACCEPTED_DATES=""
for i in $(seq 0 7); do
ACCEPTED_DATES="$ACCEPTED_DATES $(date -d @$(( NOW - i * 86400 )) +%Y-%m-%d)"
done
log "=== Verificación de backups en Mega ==="
log "Listando archivos en mega:k3s-backups/..."
FILES=$(rclone ls mega:k3s-backups/ --config /rclone/rclone.conf 2>&1)
log "$FILES"
log "---"
log "Fechas aceptadas (ventana de 8 días):$ACCEPTED_DATES"
for svc in $EXPECTED_SERVICES; do
FOUND=0
for d in $ACCEPTED_DATES; do
if echo "$FILES" | grep -q "${svc}_${d}"; then
FOUND=1
break
fi
done
if [ "$FOUND" -eq 1 ]; then
log "[OK] $svc — backup reciente encontrado"
else
log "[WARN] $svc — sin backup en los últimos 8 días"
ERRORS=$((ERRORS + 1))
fi
done
log "---"
TOTAL=$(echo "$FILES" | grep -c "tar.gz\|sql.gz" || true)
log "Total archivos en Mega: $TOTAL"
if [ "$ERRORS" -gt 0 ]; then
log "RESULTADO: $ERRORS servicio(s) sin backup reciente. Revisar."
exit 1
fi
log "RESULTADO: Todos los backups verificados correctamente."
kind: ConfigMap
metadata:
name: backup-scripts
namespace: backup-system
+11 -8
View File
@@ -19,7 +19,8 @@ spec:
runAsUser: 0 runAsUser: 0
containers: containers:
- name: verify - name: verify
image: rclone/rclone:latest # OJO: imagen BusyBox — verify.sh debe evitar GNU-ismos (date -d 'yesterday')
image: docker.io/rclone/rclone:1.74.3
command: ["/bin/sh", "/scripts/verify.sh"] command: ["/bin/sh", "/scripts/verify.sh"]
resources: resources:
limits: limits:
@@ -29,16 +30,18 @@ spec:
cpu: 50m cpu: 50m
memory: 64Mi memory: 64Mi
volumeMounts: volumeMounts:
- mountPath: /rclone/rclone.conf - mountPath: /rclone
name: rclone-conf name: rclone-conf
- mountPath: /scripts - mountPath: /scripts
name: scripts name: scripts
volumes: volumes:
- name: rclone-conf - name: rclone-conf
hostPath: secret:
path: /home/chemavx/.config/rclone/rclone.conf secretName: rclone-conf-infisical
type: File items:
- key: RCLONE_CONF
path: rclone.conf
- name: scripts - name: scripts
hostPath: configMap:
path: /data/backups/scripts name: backup-scripts
type: Directory defaultMode: 0755
+6 -4
View File
@@ -20,7 +20,9 @@ spec:
kubernetes.io/hostname: chemavx-k8 kubernetes.io/hostname: chemavx-k8
containers: containers:
- name: backup - name: backup
image: bitnami/kubectl:latest # bitnami/kubectl v1.36.2 — pinneado por digest: los tags de bitnami
# en Docker Hub ya no son fiables (migración Broadcom/bitnamilegacy)
image: docker.io/bitnami/kubectl@sha256:558420daf32bbc382e3e9af4537f4073085b336ddd47399a3b70e70087115978
command: ["/bin/bash", "/scripts/backup.sh"] command: ["/bin/bash", "/scripts/backup.sh"]
env: env:
- name: KUBECONFIG - name: KUBECONFIG
@@ -62,9 +64,9 @@ spec:
path: /data/openclaw path: /data/openclaw
type: DirectoryOrCreate type: DirectoryOrCreate
- name: scripts - name: scripts
hostPath: configMap:
path: /data/backups/scripts name: backup-scripts
type: Directory defaultMode: 0755
- name: k3s-storage - name: k3s-storage
hostPath: hostPath:
path: /var/lib/rancher/k3s/storage path: /var/lib/rancher/k3s/storage
+52
View File
@@ -0,0 +1,52 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: rclone-mega-backup
namespace: backup-system
spec:
schedule: "0 3 * * 0"
concurrencyPolicy: Forbid
failedJobsHistoryLimit: 2
successfulJobsHistoryLimit: 3
jobTemplate:
spec:
template:
spec:
nodeSelector:
kubernetes.io/hostname: chemavx-k8
restartPolicy: OnFailure
securityContext:
runAsUser: 0
containers:
- name: rclone
image: docker.io/rclone/rclone:1.74.3
command: ["/bin/sh", "/scripts/rclone-mega.sh"]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- mountPath: /data/backups/backups
name: backup-dir
- mountPath: /rclone
name: rclone-conf
- mountPath: /scripts
name: scripts
volumes:
- name: backup-dir
hostPath:
path: /data/backups/backups
type: DirectoryOrCreate
- name: rclone-conf
secret:
secretName: rclone-conf-infisical
items:
- key: RCLONE_CONF
path: rclone.conf
- name: scripts
configMap:
name: backup-scripts
defaultMode: 0755
+24
View File
@@ -0,0 +1,24 @@
apiVersion: secrets.infisical.com/v1beta1
kind: InfisicalStaticSecret
metadata:
name: rclone-conf
namespace: backup-system
spec:
# Identidad compartida read-only (sin write grant) — el valor RCLONE_CONF
# se gestiona desde la UI de Infisical (proyecto homelab / prod / /backup-system).
infisicalAuthRef:
name: infisical-auth
namespace: infisical-operator
sources:
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
environmentSlug: prod
secretPath: /backup-system
syncOptions:
refreshInterval: 60s
targets:
- kind: Secret
name: rclone-conf-infisical
namespace: backup-system
creationPolicy: Owner
# El Secret lleva la key RCLONE_CONF con el contenido íntegro de rclone.conf;
# los CronJobs la montan como fichero /rclone/rclone.conf (items en el volume).