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:
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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).
|
||||||
Reference in New Issue
Block a user