From d1d2fac287c60fe03bde30a6776fe40fa6680a4c Mon Sep 17 00:00:00 2001 From: chemavx Date: Thu, 9 Jul 2026 09:33:35 +0000 Subject: [PATCH] =?UTF-8?q?feat(backup-system):=20migraci=C3=B3n=20complet?= =?UTF-8?q?a=20a=20GitOps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backup-system/argocd-app.yaml | 23 +++ backup-system/clusterrole-backup.yaml | 15 ++ backup-system/clusterrolebinding-backup.yaml | 12 ++ backup-system/configmap-backup-scripts.yaml | 143 +++++++++++++++++++ backup-system/cronjob-backup-verify.yaml | 19 +-- backup-system/cronjob-backup.yaml | 10 +- backup-system/cronjob-rclone-mega.yaml | 52 +++++++ backup-system/infisical-rclone-conf.yaml | 24 ++++ 8 files changed, 286 insertions(+), 12 deletions(-) create mode 100644 backup-system/argocd-app.yaml create mode 100644 backup-system/clusterrole-backup.yaml create mode 100644 backup-system/clusterrolebinding-backup.yaml create mode 100644 backup-system/configmap-backup-scripts.yaml create mode 100644 backup-system/cronjob-rclone-mega.yaml create mode 100644 backup-system/infisical-rclone-conf.yaml diff --git a/backup-system/argocd-app.yaml b/backup-system/argocd-app.yaml new file mode 100644 index 0000000..a7d7b96 --- /dev/null +++ b/backup-system/argocd-app.yaml @@ -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 diff --git a/backup-system/clusterrole-backup.yaml b/backup-system/clusterrole-backup.yaml new file mode 100644 index 0000000..922a3ae --- /dev/null +++ b/backup-system/clusterrole-backup.yaml @@ -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 diff --git a/backup-system/clusterrolebinding-backup.yaml b/backup-system/clusterrolebinding-backup.yaml new file mode 100644 index 0000000..58237de --- /dev/null +++ b/backup-system/clusterrolebinding-backup.yaml @@ -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 diff --git a/backup-system/configmap-backup-scripts.yaml b/backup-system/configmap-backup-scripts.yaml new file mode 100644 index 0000000..12d5cf1 --- /dev/null +++ b/backup-system/configmap-backup-scripts.yaml @@ -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 diff --git a/backup-system/cronjob-backup-verify.yaml b/backup-system/cronjob-backup-verify.yaml index df632b2..ec1d177 100644 --- a/backup-system/cronjob-backup-verify.yaml +++ b/backup-system/cronjob-backup-verify.yaml @@ -19,7 +19,8 @@ spec: runAsUser: 0 containers: - 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"] resources: limits: @@ -29,16 +30,18 @@ spec: cpu: 50m memory: 64Mi volumeMounts: - - mountPath: /rclone/rclone.conf + - mountPath: /rclone name: rclone-conf - mountPath: /scripts name: scripts volumes: - name: rclone-conf - hostPath: - path: /home/chemavx/.config/rclone/rclone.conf - type: File + secret: + secretName: rclone-conf-infisical + items: + - key: RCLONE_CONF + path: rclone.conf - name: scripts - hostPath: - path: /data/backups/scripts - type: Directory + configMap: + name: backup-scripts + defaultMode: 0755 diff --git a/backup-system/cronjob-backup.yaml b/backup-system/cronjob-backup.yaml index 15cf17a..831d24c 100644 --- a/backup-system/cronjob-backup.yaml +++ b/backup-system/cronjob-backup.yaml @@ -20,7 +20,9 @@ spec: kubernetes.io/hostname: chemavx-k8 containers: - 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"] env: - name: KUBECONFIG @@ -62,9 +64,9 @@ spec: path: /data/openclaw type: DirectoryOrCreate - name: scripts - hostPath: - path: /data/backups/scripts - type: Directory + configMap: + name: backup-scripts + defaultMode: 0755 - name: k3s-storage hostPath: path: /var/lib/rancher/k3s/storage diff --git a/backup-system/cronjob-rclone-mega.yaml b/backup-system/cronjob-rclone-mega.yaml new file mode 100644 index 0000000..aa3597a --- /dev/null +++ b/backup-system/cronjob-rclone-mega.yaml @@ -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 diff --git a/backup-system/infisical-rclone-conf.yaml b/backup-system/infisical-rclone-conf.yaml new file mode 100644 index 0000000..8770f34 --- /dev/null +++ b/backup-system/infisical-rclone-conf.yaml @@ -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).