VAULT - RECUPERAR EL QUORUM DE RAFT

Hay errores que enseñan más que cualquier tutorial. Reduciendo un Vault de 5 a 3 réplicas en un entorno de pruebas de GKE, me cargué el quorum de Raft. Te cuento cómo lo recuperé con un `peers.json`, y cuándo no debes tocarlo.

VAULT - RECUPERAR EL QUORUM DE RAFT

CUANDO EL CLÚSTER SE QUEDA SIN LÍDER

Hay errores que enseñan más que cualquier tutorial. Reduciendo un Vault de 5 a 3 réplicas en un entorno de pruebas de GKE, me cargué el quorum de Raft. Te cuento cómo lo recuperé con un peers.json, y cuándo no debes tocarlo.

Después repetí el incidente y el procedimiento en un clúster local (kind, Vault 1.20.4) para comprobar cada paso, y salieron un par de trampas que el procedimiento oficial no te cuenta. Las verás marcadas como "ojo" a lo largo del post.

Qué había pasado

Estaba repartiendo Vault en varias zonas de GKE, con antiafinidad obligatoria y topologySpreadConstraints. Los volúmenes están ligados a la zona y los nodos de las zonas nuevas no existían todavía, así que para forzar al autoscaler a crearlos tuve que borrar el volumen del pod que quería mover.

Antes había bajado de 5 a 3 réplicas de golpe, eliminando vault-4 y vault-3 junto con sus PVC y PV, y sin sacarlos de Raft con remove-peer. Pensaba que Vault se reajustaría solo y que me quedaría tolerancia para perder un nodo. En realidad Raft seguía contando 5 votantes, con quorum de 3 y 3 pods vivos: tolerancia 0. Al borrar el volumen de vault-2 para moverlo de zona quedaron vault-0 y vault-1, 2 de 5, y el clúster se quedó sin líder.

Lo primero que probé fue escalar hacia abajo, pero enseguida me puse a leer la documentación en vez de seguir probando a ciegas. Al final lo arreglé con el procedimiento oficial de peers.json que te cuento aquí. El procedimiento correcto de reducción lo tienes en Vault - Reducir de 5 a 3 réplicas.

Qué es el quorum y cuándo se pierde

Vault replica los datos con Raft (almacenamiento integrado) y necesita mayoría simple de nodos para elegir líder. La fórmula oficial es ceil((N + 1) / 2):

Nodos Quorum Tolerancia a fallos
1 1 0
3 2 1
5 3 2

En mi caso la configuración de Raft decía 5 votantes y solo había 2 pods vivos. Con menos de 3 no hay mayoría, no hay líder y las llamadas a la API devuelven un HTTP 500, incluido vault operator raft list-peers. Ojo, que un 500 también puede venir de un problema de TLS o de almacenamiento, así que confírmalo siempre con vault status antes de tocar nada.

Cuándo usar este procedimiento (y cuándo no)

Es un último recurso. Úsalo solo si se cumple todo esto:

  • No se puede elegir líder: vault status muestra HA Mode: standby y no hay nodo activo. Ojo: el Active Node Address puede salir como <none>, pero en mi prueba local enseñaba la IP del líder que acababa de perder. No te fíes solo de ese campo: lo que manda es que list-peers falle con un 500.
  • list-peers falla o no devuelve configuración.
  • Los nodos que siguen vivos no suman quorum y los que faltan no se van a recuperar. En mi caso quedaban 2 pods con el PVC en Bound, de 5 votantes.

No lo uses si el fallo es una partición de red, si los pods están reiniciándose pero no perdidos, o si el líder sigue alcanzable. Si hay dos o más nodos vivos pero aislados, aplicar esto puede provocar un split-brain. Confirma primero que los nodos que faltan están realmente perdidos.

Paso 1: encontrar al superviviente y hacer copia

En los comandos uso una variable con el nombre del StatefulSet (y <namespace> para el namespace, que sustituyes tú). Fíjala en tu terminal antes de empezar; la necesitarás en los pasos 3 y 4:

vault_name="vault"   # nombre del StatefulSet y prefijo de los pods

Vamos a recuperar desde un único nodo. Elige como superviviente un pod que tenga el PVC en Bound y devuelva Initialized: true, Sealed: false y HA Mode: standby. vault status no necesita token:

kubectl get pvc -n <namespace>
kubectl exec ${vault_name}-0 -n <namespace> -- vault status

Antes de seguir, una copia de los datos de Raft. Es una copia mejor que nada, no una garantía. La probé restaurándola en un pod aparte, con la misma imagen: tras aplicarle un peers.json arrancó, se desselló con la clave original y conservaba mis datos (una política creada antes). Pero fue con el clúster sin escrituras; un tar con Vault en marcha puede dejar los ficheros de Raft inconsistentes, y un snapshot (vault operator raft snapshot save) no funciona sin líder. Además, necesita tar en la imagen (la de Vault lo trae con busybox):

kubectl exec ${vault_name}-0 -n <namespace> -- tar czf /tmp/vault-raft-backup.tar.gz /vault/data
kubectl cp <namespace>/${vault_name}-0:/tmp/vault-raft-backup.tar.gz ./vault-raft-backup.tar.gz

Paso 2: parar el resto de pods vivos

La documentación pide parar los servidores que sigan en marcha pero no estén sanos, y en un clúster de 5 nodos o con nodos no votantes, parar también los demás nodos antes de recuperar. En mi caso quedaban 2 pods vivos con PVC válido (vault-0 y vault-1) y, si dejas el segundo en marcha, sigue con la configuración de 5 peers. Para el procedimiento con un solo superviviente hay que dejarlo parado.

Si has borrado el volumen de un pod, el StatefulSet lo recrea con un volumen nuevo y vacío (en mi prueba local, Initialized: false, sin unirse a nada). Tampoco te sirve de superviviente, y también se para con el escalado.

Como el StatefulSet elimina los pods en orden inverso, escalar a 1 deja solo ${vault_name}-0:

kubectl scale statefulset ${vault_name} --replicas=1 -n <namespace>
kubectl wait --for=delete pod/${vault_name}-1 -n <namespace>

Esto da por hecho que tu superviviente es el -0, como en mi caso. Si no lo es, el escalado no te sirve, porque siempre se lleva los ordinales más altos, y habría que parar los demás pods de otra forma que aquí no cubro.

Los pods que paras conservan datos de Raft con la configuración antigua. Más abajo verás qué hacer si luego no se unen.

Paso 3: el node-id y la dirección

Lee el identificador del nodo, un UUID que Vault genera la primera vez que arranca, y deja en variables locales las dos cosas que necesita el fichero del paso siguiente:

node_id=$(kubectl exec ${vault_name}-0 -n <namespace> -- cat /vault/data/node-id)
echo "${node_id}"
# Ejemplo: 5bdda2aa-8079-7887-230b-f974c2cd3487

La dirección es la del servicio headless por el puerto 8201, que es el del clúster (no el 8200 de la API):

address="${vault_name}-0.${vault_name}-internal.<namespace>.svc.cluster.local:8201"

Paso 4: el peers.json

Crea en el directorio de datos de Raft un peers.json en el que solo figure el nodo superviviente. Ojo con cómo lo creas: lo hago desde tu terminal, pasando el contenido por la entrada estándar. La primera vez que lo probé en local lo escribí con un cat > ... << EOF dentro del pod, y ahí ${vault_name} no existe, así que la dirección quedó como -0.-internal.<namespace>.svc.cluster.local:8201:

kubectl exec -i ${vault_name}-0 -n <namespace> -- sh -c 'cat > /vault/data/raft/peers.json' << EOF
[
  {
    "id": "${node_id}",
    "address": "${address}",
    "non_voter": false
  }
]
EOF

# Comprueba que lo que se ha escrito es lo que esperas
kubectl exec ${vault_name}-0 -n <namespace> -- cat /vault/data/raft/peers.json

El id es el UUID del paso 3, la address es la del servicio headless y non_voter debe ser false para que el nodo pueda ser líder. Revisa el cat antes de seguir, porque Vault no se queja de una dirección mala: en mi prueba con la dirección rota aceptó el fichero, eligió líder y todo parecía en orden. El problema asomó después, al cambiar el líder: los demás nodos intentaban resolver esa dirección absurda, el nodo se quedó sin contacto y la tolerancia a fallos cayó a 0. Y el fichero se borra igualmente, así que si te equivocas tendrás que crearlo de nuevo (y repetir el paso 5). Si quieres una copia, guárdala antes de reiniciar.

Paso 5: reiniciar y comprobar

Borra el pod vault-0. Kubernetes lo recrea y Vault lee el fichero cuando el nodo queda desellado: con auto-unseal debería ser al arrancar (no lo he probado, mi clúster local no lo tenía), pero si no lo tienes, el fichero sigue ahí mientras el pod esté sellado y tendrás que hacer unseal a mano tras el reinicio:

kubectl delete pod ${vault_name}-0 -n <namespace>
kubectl get pods -n <namespace> -w

Cuando esté Running, mira en los logs que lo ha procesado:

kubectl logs ${vault_name}-0 -n <namespace> | grep -i 'raft recovery'

Deberías ver que inicia la recuperación, encuentra la nueva configuración y borra el peers.json. Que desaparezca el fichero es la confirmación de que se aplicó. Es un mecanismo de emergencia de un solo uso.

Paso 6: ¿hay líder?

vault status no necesita token, pero list-peers sí. Ojo: un export VAULT_TOKEN en tu terminal no llega al contenedor, así que pásalo en el propio comando (o entra en el pod y haz vault login):

kubectl exec ${vault_name}-0 -n <namespace> -- vault status
kubectl exec ${vault_name}-0 -n <namespace> -- env VAULT_TOKEN=<tu-token> vault operator raft list-peers

Al principio puede salir como standby mientras termina el auto-unseal. Espera unos segundos y repite hasta ver HA Mode: active. En list-peers debería aparecer un único nodo como leader.

Devolverle la alta disponibilidad

Aquí está la trampa más peligrosa del procedimiento. Parece que bastaría con volver a escalar y que retry_join hiciera el resto, pero los pods que paraste en el paso 2 conservan en su PVC la configuración de Raft de antes, y eso se puede volver contra ti.

En mi prueba local, tras recuperar un clúster de 3 votantes con un solo superviviente, subí a 3 sin tocar nada. Los dos pods que vuelven, con su configuración antigua, suman mayoría entre ellos (2 de 3), así que formaron otro clúster, con su propio líder. Resultado: dos pods con HA Mode: active, cada uno con una vista distinta en list-peers, y los dos con la etiqueta vault-active=true por la que el servicio vault-active elige a quién enviar el tráfico, de modo que las peticiones podían caer en clústeres distintos. Es un split-brain, el mismo riesgo que te avisaba al principio, pero provocado por el propio procedimiento. En mi prueba local con 5 votantes y un pod viejo no ocurrió: ese pod solo, con su configuración de 5, no suma mayoría. Pero depende de cuántos nodos viejos vuelvan, y no tiene sentido jugársela.

Lo que haría por defecto es dejar los demás pods limpios antes de subirlos. Siguen parados desde el paso 2, así que el PVC se borra al momento (los nombres dependen de tu chart, míralos antes con kubectl get pvc). Esto no se hace nunca sobre el pod del superviviente, ni sobre el líder:

kubectl get pvc -n <namespace>
kubectl delete pvc data-${vault_name}-1 data-${vault_name}-2 -n <namespace>
kubectl scale statefulset ${vault_name} --replicas=3 -n <namespace>

Arrancan vacíos, se unen al líder por retry_join como peers nuevos y replican todo desde él (sin auto-unseal tendrás que desellarlos a mano; en mi prueba hizo falta hacerlo dos veces: la primera respuesta seguía diciendo Sealed true). En mi prueba acabaron con 3 votantes, Healthy: true y Failure Tolerance: 1.

Si ya habías subido sin borrar nada, comprueba cuanto antes que no hay dos activos (lo cuento en el apartado siguiente). Si los hay, baja de nuevo a 1 réplica, borra los PVC de los otros y vuelve a subir. Y si un nodo con datos viejos no tiene a nadie con quien formar mayoría, no se une pero tampoco avisa: en mi prueba vault status lo enseñaba como standby y sin sellar, y solo list-peers revelaba que no estaba en el clúster (en sus logs, elecciones repetidas contra los nodos antiguos).

Antes de dar el tema por cerrado

Un vault status normal no te dice si el clúster está sano de verdad. El comando que sí lo hace es:

kubectl exec ${vault_name}-0 -n <namespace> -- env VAULT_TOKEN=<tu-token> vault operator raft autopilot state

Fíjate en que Healthy sea true, que Failure Tolerance sea 1 o más (si es 0, funciona pero no aguanta ni la caída de un nodo) y que todos los nodos salgan alive y como Voter.

Pero esto solo mira el clúster desde el pod donde lo lanzas. En el split-brain de mi prueba, autopilot state desde el superviviente decía Healthy: true (con 1 votante y tolerancia 0) y no sabía nada del otro clúster. Por eso conviene hacer dos comprobaciones más:

# Lanza list-peers desde cada pod: tienen que dar la misma lista
for i in 0 1 2; do
  kubectl exec ${vault_name}-$i -n <namespace> -- env VAULT_TOKEN=<tu-token> vault operator raft list-peers
done

# Un único pod con vault-active=true
kubectl get pods -n <namespace> -L vault-active

Mira también la columna Address: todas las direcciones deben tener nombres de pod válidos. Una como -0.-internal... es la señal de que el peers.json se escribió mal.

También conviene saber qué riesgo hay. La documentación avisa de que el peers.json puede hacer que se den por confirmadas entradas del log de Raft que nunca llegaron a confirmarse. Y como has perdido varios servidores, el estado confirmado que te queda puede estar incompleto. Es decir, puede faltar alguna escritura reciente o aparecer alguna que no se llegó a confirmar. Por eso es el último recurso y por eso no se automatiza. Los secretos guardados hace tiempo siguen ahí mientras el PVC del superviviente esté intacto (en mi prueba local, una política creada antes de la recuperación seguía ahí después de dos recuperaciones seguidas); lo que puede fallar son las escrituras recientes.

Para cerrar

Lo que me llevo de esto es que habría podido ahorrármelo, por dos motivos. El primero, que había que bajar de uno en uno, sacando cada peer de Raft con remove-peer y comprobando con autopilot state que la tolerancia a fallos seguía siendo 1. El segundo, que si no hubiera borrado vault-4 y vault-3 con sus volúmenes, me habría bastado con subir de nuevo a 5 réplicas y recuperar el clúster sin tocar peers.json. Eso solo funciona si los PVC siguen existiendo y no has ejecutado remove-peer sobre esos pods: después, sus volúmenes tienen datos obsoletos y hay que borrarlos antes de volver a subir. Con el clúster recuperado habría podido descubrir con calma el problema antes de seguir con la distribución en 3 zonas. Borrar lo que sobra demasiado pronto me quitó esa salida.

Si te toca usarlo, saca la copia antes de tocar nada.

Documentación oficial:


Tweet Send
0 Comentarios
Cargando...