Operación
Procedimientos para el día a día. Todos los comandos kubectl dan por hecho un kubeconfig que apunta a la VIP (https://192.0.2.250:6443).
Estado general
scripts/check_cluster.sh hace una revisión de solo lectura: nodos, miembros de etcd, VIP, keepalived, pods con problemas, volúmenes de Longhorn y Velero.
./scripts/check_cluster.sh
Para las actualizaciones del sistema de los nodos, el portal de actualizaciones (scripts/mantenimiento.sh). Para la seguridad, scripts/seguridad.py (ver seguridad).
Comprobaciones a mano:
kubectl get nodes -o wide
kubectl get pods -A | grep -vE 'Running|Completed'
kubectl -n longhorn-system get volumes.longhorn.io # robustness: healthy
sudo k3s etcd-snapshot list # en un master
journalctl -u k3s --since '1 hour ago' | grep -iE 'leader|elect|timeout' | tail
Señales de que etcd sufre:
leaderelection losten los logs de k3s.- Un aumento brusco de
NRestartsensystemctl show k3s -p NRestarts. - Muchos reinicios simultáneos en los operadores (Longhorn CSI, Kyverno, cert-manager…).
Mantenimiento de un nodo (drain y reboot)
kubectl drain <nodo> --ignore-daemonsets --delete-emptydir-data
# … mantenimiento / reboot …
kubectl uncordon <nodo>
Antes de hacer drain:
- Comprueba qué tiene
local-pathen ese nodo (almacenamiento.md). Esas aplicaciones no se moverán: quedarán enPendinghasta que vuelva el nodo.- master-1: Postgres central, Postgres de Jira, Pi-hole (se cae el DNS de la LAN), n8n y Vector (por
nodeSelector). - master-2: Kafka y Jenkins.
- master-1: Postgres central, Postgres de Jira, Pi-hole (se cae el DNS de la LAN), n8n y Vector (por
- Si es un master:
- Los otros dos tienen que estar
Readyy etcd sano (3 miembros). Nunca reinicies dos masters a la vez. - Si es master-1, comprueba antes que la VIP tiene relevo (keepalived en
BACKUPen otro master, no enFAULT). Si no lo tiene, usa la IP de otro master en el kubeconfig mientras dure el mantenimiento.
- Los otros dos tienen que estar
- Si es el worker-1 (HDD), espera a que Longhorn termine de reconstruir réplicas antes de volver a tocarlo.
Si el drain se queda esperando por el PDB de instance-manager de Longhorn, suele ser un ticket
snapshot-controller-* huérfano en un volumeattachments.longhorn.io: apunta a un snapshot que ya
no existe (pasó tras la caída del 27/10/2026). Se quita del spec.attachmentTickets del volumen, solo
si el snapshot no aparece en kubectl -n longhorn-system get snapshots.longhorn.io. No borra datos.
Ver incidente del 28/10.
# Tickets de snapshot y sus volúmenes
kubectl -n longhorn-system get volumeattachments.longhorn.io -o json \
| jq -r '.items[]|.metadata.name as $v|(.spec.attachmentTickets//{})|keys[]|select(startswith("snapshot-controller-"))|"\($v) \(.)"'
# Para cada uno cuyo snapshot (el nombre sin "snapshot-controller-") no exista:
kubectl -n longhorn-system patch volumeattachments.longhorn.io <volumen> --type json \
-p '[{"op":"remove","path":"/spec/attachmentTickets/<ticket>"}]'
Añadir un worker
Ver nodes/README.md. Después:
kubectl label node <nodo> node-role.kubernetes.io/worker=worker
Convertir un worker en master
- Copia antes
/var/lib/rancher/k3s/storage/: el desinstalador de agent lo borra sin avisar. - Haz drain del nodo y ejecuta
k3s-agent-uninstall.sh. - Instala el server con la misma versión que el clúster:
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.34.6+k3s1 K3S_TOKEN=<token> sh -s - server \ --server https://192.0.2.250:6443 --disable=servicelb \ --node-ip=<ip> --flannel-iface=<interfaz-cableada> - Añade el nodo al backend de haproxy en los masters e instala keepalived con su prioridad (nodes/masters/).
- Comprueba que
ip -d link show flannel.1 | grep localmuestra la IP real del nodo.
Con 2 miembros de etcd, el quórum son los 2. Si el tercero no termina de unirse, se pierde la API. Para recuperarla:
k3s server --cluster-reseten el master sano, borrardb/en el nodo que falló y repetir el join.
Actualizar k3s
⚠️ No uses
curl -sfL https://get.k3s.io | sh -s - serversin flags para actualizar. El script de instalación reescribe/etc/systemd/system/k3s.servicecon los flags que le pases. Sin ellos, el master pierde--node-ip,--flannel-iface,--disable=servicelb,--server… (ver nodes/README.md).
El clúster tiene 3 masters con etcd embebido: no es un k3s single (SQLite) desde la migración del 2026-07-03. La actualización se hace por turnos (rolling): un nodo cada vez, mientras los otros dos masters mantienen el quórum de etcd.
Reglas
- Primero los masters, después los workers. Nunca un worker por delante de los masters.
- Sin saltarse versiones minor: 1.34 → 1.35 → 1.36. Los parches dentro de la misma rama (1.34.6 → 1.34.12) sí se pueden saltar.
- Un master cada vez, comprobando etcd antes de pasar al siguiente. Orden recomendado: master-1 → master-2 → master-3 (los tres por cable desde el 2026-10-27). Con master-1 parado, el quórum lo mantienen los dos masters cableados, que se ven entre sí sin pérdidas.
- Para parches no hace falta
drain: al reiniciar k3s, los contenedores siguen corriendo (containerd los mantiene). Así se evita mover las aplicaciones atadas a un nodo porlocal-path. Para un cambio de rama (minor), sí:drainantes yuncordondespués. - Antes de cambiar de rama, comprobar la compatibilidad de Longhorn, Strimzi, ECK, Argo CD, Kyverno, Velero y Falco, y buscar APIs retiradas (por ejemplo con kubent o pluto).
Desde el portal (recomendado)
Botón «6 · Actualizar k3s» de Semaphore (ansible/README.md), o ./scripts/mantenimiento.sh k3s -e version=v1.34.12+k3s1 (con --check antes). Hace este mismo procedimiento: masters primero de uno en uno (snapshot de etcd, espera a Ready con la versión nueva y [+]etcd ok) y después los workers, que no se tocan si algún master va por detrás. nodo-mantenimiento k3s valida la versión (formato, nunca hacia atrás, como mucho una rama más, binario de GitHub con su sha256). Para un cambio de rama hace drain y uncordon, pero la revisión de compatibilidades sigue siendo manual.
A mano, en un nodo: scripts/k3s_parche.sh (solo parches; lanzarlo con systemd-run y la ruta completa del script).
Procedimiento por nodo (sustituyendo solo el binario)
Conserva el unit de systemd y todos los flags.
V=v1.34.12+k3s1 # versión de destino
ARCH=amd64; BIN=k3s # en la Raspberry: ARCH=arm64; BIN=k3s-arm64
U="https://github.com/k3s-io/k3s/releases/download/${V/+/%2B}"
# 1. Descargar y verificar
curl -sfLo /tmp/$BIN "$U/$BIN"
curl -sfL "$U/sha256sum-$ARCH.txt" | grep " $BIN\$" | sed "s| $BIN\$| /tmp/$BIN|" | sha256sum -c -
# 2. Solo en masters: snapshot de etcd antes de tocar nada
sudo k3s etcd-snapshot save --name pre-$(echo $V | tr + -)
# 3. Sustituir el binario y reiniciar
sudo install -m 755 /tmp/$BIN /usr/local/bin/k3s
sudo systemctl restart k3s # en workers: k3s-agent
Comprobaciones antes de pasar al siguiente nodo:
kubectl get nodes -o wide # el nodo, Ready y con la versión nueva
kubectl get --raw '/readyz?verbose' | grep -E 'etcd|failed' # [+]etcd ok
./scripts/check_cluster.sh
Si el SSH va mal: lanzar la actualización desacoplada con systemd-run
Si la conexión SSH se corta (no solo va lenta), el servidor envía SIGHUP a la sesión y mata el script a medias. Para que termine pase lo que pase, se lanza como servicio temporal de systemd:
sudo -v # meter la contraseña antes, para que no la pida a mitad
sudo systemd-run --unit=k3s-upgrade --collect bash -c '
set -e; V=v1.34.12+k3s1; BIN=k3s; ARCH=amd64 # Raspberry: BIN=k3s-arm64; ARCH=arm64
U="https://github.com/k3s-io/k3s/releases/download/${V/+/%2B}"
curl -sfLo /tmp/$BIN "$U/$BIN"
curl -sfL "$U/sha256sum-$ARCH.txt" | grep " $BIN$" | sed "s| $BIN$| /tmp/$BIN|" | sha256sum -c -
# solo en masters: k3s etcd-snapshot save --name pre-$(echo $V | tr + -)
cp -a /usr/local/bin/k3s /usr/local/bin/k3s.anterior.bak
install -m 755 /tmp/$BIN /usr/local/bin/k3s
systemctl restart k3s-agent # en masters: k3s
echo "OK: actualizado a $V"'
journalctl -u k3s-upgrade -f # ver el progreso (también después de reconectar)
El resultado se comprueba también sin SSH, por la API: kubectl get nodes -o wide o la tarjeta Nodos de Homepage.
Esto protege el script, no el clúster: con la red cableada perdiendo paquetes, no actualices masters (el quórum de etcd depende de los otros dos). Mide antes:
ping -c 60 -i 0.5 192.0.2.1desde un nodo cableado debe dar 0 % de pérdida.
En Homepage (http://192.0.2.242), la tarjeta Nodos muestra la versión de cada nodo y Control-plane el número de versiones distintas (2 mientras dura la actualización y 1 al terminar).
Después de actualizar un master, revisa que local-path no ha vuelto a quedar como StorageClass por defecto.
Marcha atrás de un parche: reinstalar el binario anterior igual que en el paso 3. Si etcd se corrompiera: k3s server --cluster-reset --cluster-reset-restore-path=<snapshot pre-…> (ver más abajo).
Actualizaciones del sistema de los nodos
Con el portal de actualizaciones: el panel de Grafana Actualizaciones de los nodos muestra qué falta, y ./scripts/mantenimiento.sh actualizar lo instala nodo a nodo, con drain y reinicio si hace falta.
- Requiere el colector textfile de node-exporter, activado con
helm upgrade --reuse-values -f ansible/kubernetes/node-exporter-textfile.yamlsobre la releasemonitoring/prometheus. En cadahelm upgradefuturo de kube-prometheus-stack hay que volver a pasar ese fichero (o usar--reuse-values), o el panel se queda sin datos. - Antes de reiniciar un master, el playbook comprueba que la red hasta él es estable (30 conexiones a la API sin fallos), por la VIP y etcd.
- Al reiniciar un nodo se paran hasta que vuelve los servicios con volúmenes
local-pathen él (el playbook los lista antes) y se pierde el contenido de losemptyDirde sus pods, como el histórico de Prometheus, que no tiene volumen persistente (Grafana sí lo tiene desdegrafana_persistente.sh).
Grafana: volumen persistente
Grafana guarda paneles, usuarios, carpetas y alertas creados a mano en /var/lib/grafana (grafana.db). Hasta el 2026-10-26 era un emptyDir, que se pierde cada vez que el pod cambia de nodo. scripts/grafana_persistente.sh lo pasa a un PVC de Longhorn sin perder nada:
- Copia
grafana.dba/root/.config/grafana-copia/(solo root) y comprueba que su sha256 coincide con la del pod; si la red corta la copia, reintenta. Los plugins no se copian: Grafana los vuelve a instalar al arrancar. helm upgrade --reuse-valuesconmonitoring/grafana-persistente.yaml(y el colector textfile de node-exporter).- Aparta el
grafana.dbvacío del volumen nuevo, sube la copia en trozos de 48 KB (por la entrada estándar dekubectl execllega cortada), comprueba su huella y arranca Grafana.
Aplicado el 2026-10-26: PVC prometheus-grafana en Longhorn, base de datos restaurada con huella idéntica y la copia en /root/.config/grafana-copia/.
Los datasources de Prometheus y Alertmanager los crea el chart y no dependen de esto.
- No toques Grafana mientras corre el script. Lo que se cree entre la copia (paso 1) y el
helm upgrade(paso 2) se pierde: la estrategiaRecreateborra el pod viejo antes de restaurar. - Marcha atrás:
helm upgrade --reuse-values --set grafana.persistence.enabled=falsedeja Grafana con unemptyDirvacío. Hay que restaurar el.tara mano (kubectl exec -i … -- tar xf - -C /var/lib/grafana). El PVCmonitoring/prometheus-grafanano se borra solo: bórralo a mano cuando ya no haga falta.
Grafana: «database is locked»
El 2026-10-26 (con el volumen recién creado y 3 réplicas en master-2, worker-2 y master-3), un corte de red con la réplica de master-3 dejó las escrituras colgadas unos segundos y Grafana empezó a fallar con database is locked (SQLite es muy sensible a eso). Se recuperó sola cuando Longhorn siguió con las otras réplicas. Para que no dependa de la red, scripts/grafana_local.sh deja el volumen con 2 réplicas y una siempre en el nodo de Grafana (dataLocality: best-effort), quitando la que sobre (la de master-3 si se puede, nunca la local). Grafana tiene que correr en un nodo cableado (fijado en worker-2): en el master-1 (WiFi) la réplica local no llega a reconstruirse (ver incidentes). Es un cambio en el volumen de Longhorn, no en Helm: un helm upgrade no lo deshace, pero recrear el PVC (otro grafana_persistente.sh) lo devuelve a 3 réplicas sin localidad.
Exportar el estado del clúster a este repo
python3 scripts/export_cluster.py # regenera cluster/ (sin secretos)
git diff --stat # revisa los cambios
git add -A && git commit -m "Exportación del estado del clúster $(date +%F)"
El script falla si detecta algo que parezca una clave privada o un token. Revisa siempre el diff antes de hacer push.
Cambiar la interfaz VRRP de keepalived
Hace falta cuando un master cambia de red física, por ejemplo al pasar de WiFi a cable. (Desde el 2026-10-27 los 3 masters anuncian por cable; master-1 se pasó con scripts/master1_a_cable.sh, que además cambia su IP y k3s.) Si interface apunta a una interfaz caída, keepalived entra en FAULT y ese master no puede tomar la VIP.
Requisito previo: la red cableada tiene que estar estable. Si un BACKUP deja de oír los anuncios del MASTER durante más de unos 3 s, se queda también con la VIP (split-brain). Con cortes periódicos en la red, la VIP iría saltando de un nodo a otro. Comprobación desde master-1:
ping -D -O -c 90 192.0.2.139 # master-2: 0 % de pérdida y ningún "no answer yet" ping -D -O -c 90 192.0.2.129 # master-3: lo mismo, antes de tocarloCuando se escribió, master-1 salía por WiFi, así que esto medía el camino WiFi + cable: servía para detectar los cortes, pero no era una medida solo del cable. Para medir solo el cable, desde master-2:
ping -D -O -c 90 192.0.2.1 # router
Valores para cada nodo:
| Nodo | Interfaz antigua (WiFi, caída) | Interfaz nueva (cable) |
|---|---|---|
master-1 |
wlp3s0 |
enp4s0f0 (con master1_a_cable.sh) |
master-2 |
wlp2s0 |
enp1s0 |
master-3 |
wlo1 |
enp2s0 |
✅ Hecho el 2026-10-27 en los dos nodos. scripts/keepalived_cable.sh automatiza estos pasos y se para si algo no está bien (sudo bash keepalived_cable.sh enp1s0 en master-2, enp2s0 en master-3). A mano:
Se hace de un nodo en uno, primero master-2 y después master-3. En cada nodo, con sudo:
NUEVA=enp1s0 # enp2s0 en master-3
# 1. ¿Llegan por cable los anuncios VRRP de master-1 (192.0.2.112, prio 150)?
# Tienen que salir ~10 paquetes, uno por segundo. Si no sale nada, PARAR: el BACKUP no oiría al MASTER.
sudo timeout 12 tcpdump -ni "$NUEVA" -c 10 vrrp
# 2. Copia de seguridad y cambio
sudo cp -a /etc/keepalived/keepalived.conf /etc/keepalived/keepalived.conf.bak-$(date +%F)
sudo sed -i -E "s/^(\s*interface\s+)\S+/\1$NUEVA/" /etc/keepalived/keepalived.conf
grep -n interface /etc/keepalived/keepalived.conf # REVISAR: una sola línea y con la interfaz nueva.
# El sed cambia TODAS las líneas "interface"; si hubiera
# más de un vrrp_instance, restaurar la copia y editar a mano.
# 3. Validar la sintaxis y aplicar.
# keepalived -t devuelve código 5 por el aviso "Truncating auth_pass to 8 characters"
# (la contraseña VRRP actual es más larga; se recorta igual en los 3 nodos). Ese aviso se ignora;
# cualquier otro mensaje (p. ej. "Non-existent interface") detiene el procedimiento.
out=$(sudo keepalived -t -f /etc/keepalived/keepalived.conf 2>&1); echo "$out"
if echo "$out" | grep -v 'Truncating auth_pass' | grep -q .; then
echo "REVISAR la configuración: NO se reinicia"
else
sudo systemctl restart keepalived
fi
# 4. Comprobar: tiene que decir "Entering BACKUP STATE", nunca MASTER ni FAULT
journalctl -u keepalived -n 20 --no-pager | grep -i state
ip -br addr show dummy0 # NO debe aparecer 192.0.2.250 en este nodo
Después, desde master-1:
ip -br addr show dummy0 # aquí SÍ: 192.0.2.250/32
./scripts/check_cluster.sh # keepalived: .112 MASTER, .139 y .129 BACKUP
Marcha atrás, si el nodo se pone en MASTER teniendo master-1 la VIP (split-brain):
sudo cp -a /etc/keepalived/keepalived.conf.bak-<fecha> /etc/keepalived/keepalived.conf
sudo systemctl restart keepalived
Prueba de relevo (opcional, en un momento tranquilo): en master-1, sudo systemctl stop keepalived. En unos 3 s la VIP tiene que pasar a master-2 (ip -br addr show dummy0 en master-2) y kubectl get nodes tiene que seguir respondiendo. Después, sudo systemctl start keepalived en master-1: por prioridad, recupera la VIP.
Recuperación del control-plane
| Situación | Acción |
|---|---|
| Cae 1 master | Nada urgente: etcd sigue con quórum (2 de 3). Arreglar el nodo y arrancarlo. |
| Caen 2 masters | Sin quórum: la API no responde. Arrancar al menos uno más. Si no es posible, k3s server --cluster-reset en el superviviente (vuelve a 1 miembro) y volver a unir los otros con db/ borrado. |
| Pérdida total de etcd | k3s server --cluster-reset --cluster-reset-restore-path=/var/lib/rancher/k3s/server/db/snapshots/<snapshot> en un master y volver a unir el resto. |