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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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