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:

Mantenimiento de un nodo (drain y reboot)

kubectl drain <nodo> --ignore-daemonsets --delete-emptydir-data
# … mantenimiento / reboot …
kubectl uncordon <nodo>

Antes de hacer drain:

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

  1. Copia antes /var/lib/rancher/k3s/storage/: el desinstalador de agent lo borra sin avisar.
  2. Haz drain del nodo y ejecuta k3s-agent-uninstall.sh.
  3. 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>
    
  4. Añade el nodo al backend de haproxy en los masters e instala keepalived con su prioridad (nodes/masters/).
  5. Comprueba que ip -d link show flannel.1 | grep local muestra 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-reset en el master sano, borrar db/ en el nodo que falló y repetir el join.

Actualizar k3s

⚠️ No uses curl -sfL https://get.k3s.io | sh -s - server sin flags para actualizar. El script de instalación reescribe /etc/systemd/system/k3s.service con 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

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.1 desde 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.

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:

  1. Copia grafana.db a /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.
  2. helm upgrade --reuse-values con monitoring/grafana-persistente.yaml (y el colector textfile de node-exporter).
  3. Aparta el grafana.db vacío del volumen nuevo, sube la copia en trozos de 48 KB (por la entrada estándar de kubectl exec llega 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.

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.

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.