VAULT - REDUCIR DE 5 A 3 RÉPLICAS

PARECÍA QUE IBA BIEN Y NO LO ESTABA

Bajar las réplicas de un Vault con Raft parece cosa de cambiar un número en un YAML. Yo lo hice así en un entorno de pruebas de GKE, la cosa pareció ir bien, y un paso después me quedé sin quorum. Te cuento qué pasó y el procedimiento que habría tenido que seguir. Sirve para cualquier reducción, no solo de 5 a 3: uso ese caso porque es fácil de razonar.

Cómo empezó todo

La idea era tener Vault en alta disponibilidad repartido en tres zonas de GKE. Con un clúster de 3 nodos ya lo habíamos hecho y había ido bien. Pero el entorno de pruebas tenía 5 réplicas, todas en la misma zona, así que había que bajar a 3 antes de repartirlas.

Para repartirlas usábamos antiafinidad obligatoria y topologySpreadConstraints. Y había un detalle más: los volúmenes están ligados a la zona donde se crearon, y los nodos de las zonas nuevas no existían todavía, porque éramos los primeros en usarlos. Para que el autoscaler los creara había que borrar el volumen del pod que queríamos mover.

Así que lo hice en dos pasos:

  1. Bajé de 5 a 3 eliminando vault-4 y vault-3, con sus PVC y sus PV.
  2. Borré el pod vault-2 y su PV para forzar que naciera en otra zona.

El segundo paso dejó el clúster sin quorum.

Lo que yo creía y lo que era

Yo daba por hecho que Vault se gestionaba solo: al desaparecer dos pods, el clúster se reajustaría a 3 y tendría tolerancia para perder uno más. No fue así.

Raft no sabe que has apagado un pod. Para él, esos dos nodos siguen siendo votantes que no responden. Con 5 votantes el quorum es 3, y con 3 pods vivos no sobraba ninguno: la tolerancia a fallos era 0. Al borrar otro pod quedaron 2 de 5 y sin mayoría no hay líder.

Lo que creía Lo que había
Votantes en Raft 3 5
Quorum 2 3
Pods vivos 3 3
Tolerancia a fallos 1 0

Fíjate en que pasar de 5 a 3 de golpe no rompe el quorum: 3 pods vivos siguen cumpliendo el 3 de 5. Lo que rompe es el margen. Mientras los pods se están terminando no queda ninguna tolerancia, y si algo va mal en ese momento no hay por dónde recuperar. Algo parecido pasa si te saltas pasos, como ir directo de 5 a 2: ahí sí se pierde el quorum entero.

Lo comprobé después en un clúster de pruebas local (kind, Vault 1.20.4) para no fiarme solo de mi cabeza. Con 5 votantes y 4 pods, autopilot state decía Failure Tolerance: 1; con 5 votantes y 3 pods, Failure Tolerance: 0 y Healthy: false. En otra prueba, con 4 votantes y 3 pods, borrar un tercer pod (2 de 4) dejó el clúster sin líder, y ni list-peers ni autopilot state respondían: error 500.

¿Y la limpieza automática? La documentación explica que Autopilot viene activado, pero la limpieza de servidores muertos (cleanup-dead-servers) hay que activarla a mano y exige configurar min-quorum. Además, el umbral por defecto para dar un servidor por muerto es de 24 horas. En mi caso no iba a actuar a tiempo, aunque hubiera estado activa.

Con el clúster de 3 nodos de antes no nos pasó porque su configuración de Raft decía 3. Al borrar un pod quedaban 2 de 3, que sí es quorum.

El procedimiento correcto

Este es el procedimiento al que llegamos, que coincide con el del tutorial oficial de Integrated Storage. Antes de los comandos, dos reglas que me habrían ahorrado el disgusto:

Saca el peer de Raft antes de apagar el pod. Si apagas primero, Raft sigue contando un votante que no responde y te quedas sin tolerancia hasta que lo saques. Yo medí justo eso: con 4 votantes y 3 pods, Failure Tolerance: 0. Haciéndolo al revés (remove-peer y después escalar) la tolerancia no bajó de 1 en ningún paso, y acabé con 3 votantes y tolerancia 1.

Quizá pienses que con el pod todavía en marcha retry_join lo volverá a meter en el clúster y deshará lo que acabas de hacer. En mi prueba no pasó: el peer siguió fuera, y al reiniciarse el pod retry_join falló con node has been removed from the HA cluster. All vault data for this node must be cleaned up before it can be added back. Un nodo expulsado no vuelve solo.

Baja de uno en uno y comprueba entre paso y paso. Ir réplica a réplica no te da más tolerancia en sí; lo que te da es la oportunidad de mirar autopilot state antes de tocar el siguiente pod y parar si algo no cuadra.

El patrón de cada paso queda así:

step-down si es líder → remove-peer → comprobar peers → scale down → comprobar que el pod ha terminado → comprobar peers

Para no equivocarme al sustituir nombres, fijo antes una variable (el namespace lo pongo como <namespace> en cada comando):

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

Uso ${vault_name}-0 como pod desde el que lanzo los comandos. En un StatefulSet los pods se eliminan en orden inverso, así que el 0 siempre sigue ahí.

Una cosa antes de empezar: list-peers, remove-peer, autopilot state y step-down necesitan un token. Sin él, la API responde 403 permission denied. Además, step-down exige en su política sudo sobre sys/step-down: con una política que solo tenía update, también me dio 403. Un export VAULT_TOKEN en tu terminal no llega al contenedor, así que lo paso en el propio comando con env VAULT_TOKEN=<tu-token>.

Antes de empezar

Con el clúster sano. Si algo de esto falla, no sigas:

# Todos los pods Running y Ready
kubectl get pods -n <namespace>

# Sin sellar, HA activo y con líder
kubectl exec ${vault_name}-0 -n <namespace> -- vault status

# 5 peers, 1 líder y 4 followers, todos votantes
kubectl exec ${vault_name}-0 -n <namespace> -- env VAULT_TOKEN=<tu-token> vault operator raft list-peers

# Healthy: true y Failure Tolerance: 2
kubectl exec ${vault_name}-0 -n <namespace> -- env VAULT_TOKEN=<tu-token> vault operator raft autopilot state

Fase 1: el cambio en Git y frenar a ArgoCD

El cambio en sí es una línea en el manifiesto, que se sube con su merge request:

replicas: 3

Si ArgoCD tiene el auto-sync activado, en cuanto mergees empezará a bajar los pods por su cuenta, sin que tú controles el orden. Así que desactívalo antes de mergear:

ArgoCD UI → Application vault → App Details → Disable Auto-Sync

Fase 2: quitar los peers y bajar los pods

Como Vault corre en un StatefulSet, Kubernetes elimina los pods en orden inverso: primero vault-4, luego vault-3. El proceso es idéntico para cada uno y hay que terminarlo del todo antes de pasar al siguiente.

Mira primero cómo están los peers:

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

Deberías ver algo así:

Node                                   Address                         State       Voter
----                                   -------                         -----       -----
5bdda2aa-8079-7887-230b-f974c2cd3487   vault-0.vault-internal:8201     leader      true
3f1e9c12-4521-6643-ab1d-e829c1ab4512   vault-1.vault-internal:8201     follower    true
7ca2d341-9901-2234-cc3e-f112b3cd7821   vault-2.vault-internal:8201     follower    true
a91c04be-33d7-4f0a-8c52-0b6e7d19f6a3   vault-3.vault-internal:8201     follower    true
e2b7d5f0-6a18-4c93-9d41-5c8a02e7b1d4   vault-4.vault-internal:8201     follower    true

Ojo con la columna Node: no son los nombres de los pods, sino UUIDs que Vault genera la primera vez que arranca cada nodo. El remove-peer recibe ese UUID, no el nombre del pod. Para saber cuál es el de vault-4, mira la columna Address, que sí lleva el nombre del pod.

Si vault-4 es el líder, hazle antes un step-down y confirma con list-peers que otro pod ha tomado el relevo. El liderazgo podría volver al mismo nodo, así que si lo sigue siendo, repítelo:

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

Ahora sí, con el pod todavía en marcha, lo sacas de Raft con el UUID que viste en list-peers para vault-4:

kubectl exec ${vault_name}-0 -n <namespace> -- env VAULT_TOKEN=<tu-token> \
  vault operator raft remove-peer <uuid-de-vault-4>

En mi prueba el pod expulsado se reinició solo y se quedó sellado. No te asustes si lo ves: es el nodo que acabas de echar, y lo vas a apagar ahora mismo. Baja a 4 réplicas y espera a que haya desaparecido del todo:

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

Y la comprobación. list-peers era lo que usábamos para ver cómo estaba el clúster, pero no da la información que más importa en esta operación: cuánta tolerancia a fallos queda. Para eso mira también el estado de Autopilot:

kubectl exec ${vault_name}-0 -n <namespace> -- env VAULT_TOKEN=<tu-token> vault operator raft list-peers
# Esperado: 4 peers, 1 líder, 3 followers

kubectl exec ${vault_name}-0 -n <namespace> -- env VAULT_TOKEN=<tu-token> vault operator raft autopilot state
# Fíjate en: Healthy: true y Failure Tolerance: 1

Con 4 votantes el quorum sigue siendo 3, así que la tolerancia es 1. Autopilot tarda unos segundos en reflejar los cambios: a mí, justo después de apagar un pod, aún me enseñaba la tolerancia de antes. Espera un poco y vuelve a mirar antes de seguir, y no sigas hasta que ambos comandos digan lo esperado.

Repite lo mismo con vault-3: step-down si es líder, remove-peer <uuid-de-vault-3>, scale --replicas=3, kubectl wait --for=delete pod/${vault_name}-3 y la misma doble comprobación. Con 3 peers deberías ver 1 líder, 2 followers y Failure Tolerance: 1. El quorum ha ido así:

5 peers → quorum 3   (quitando vault-4)
4 peers → quorum 3   (quitando vault-3)
3 peers → quorum 2   ✅ estado final

Hasta que ejecutas remove-peer sobre un pod, su PVC conserva sus datos de Raft y es tu salida si algo se tuerce, y por eso los PVC no se borran hasta la fase 4. Después de remove-peer ya no vale: más abajo te cuento qué pasa.

Fase 3: devolverle el control a ArgoCD

Como el merge request ya dejaba replicas: 3, el estado real y el deseado coinciden. Vuelve a activar el auto-sync y ArgoCD no tendrá nada que corregir:

ArgoCD UI → Application vault → App Details → Enable Auto-Sync

Fase 4: borrar los PVC huérfanos

Cuando todo ha terminado y el clúster está sano, quedan los PVC de los pods eliminados. No se borran solos: por defecto un StatefulSet los conserva al escalar a la baja (persistentVolumeClaimRetentionPolicy con Retain). Comprueba primero que sigue todo bien:

kubectl exec ${vault_name}-0 -n <namespace> -- env VAULT_TOKEN=<tu-token> vault operator raft autopilot state
# Esperado: Healthy: true, 3 peers

Y entonces los borras. Los nombres dependen de tu chart; aquí uso data-vault-N como ejemplo, así que míralos antes con el get:

kubectl get pvc -n <namespace>
kubectl delete pvc -n <namespace> data-${vault_name}-3 data-${vault_name}-4
kubectl get pvc -n <namespace>
# Esperado: solo data-vault-0, data-vault-1 y data-vault-2

Termina con un vault status para confirmar que el HA sigue activo y no hay ningún standby atascado.

Los volúmenes y la red de seguridad

Aquí está lo que yo haría distinto: no borres los PVC antes de la fase 4. Yo eliminé los de vault-3 y vault-4 en cuanto los bajé, y con eso me quedé sin salida.

Los PVC son tu mecanismo de recuperación, pero solo mientras no hayas ejecutado remove-peer sobre ese pod. Hay dos situaciones:

Antes de remove-peer. El pod sigue siendo un peer válido de Raft y su PVC conserva los datos. Basta con volver a escalar y se reincorpora solo. Este fue mi caso: no había ejecutado remove-peer en ningún momento, así que con los volúmenes intactos me habría bastado subir a 5 y recuperar el clúster, y después investigar con calma antes de seguir con las tres zonas. Lo probé en el clúster local: tras perder el quorum, el pod volvió con su PVC y entró como follower sin tocar nada más (sin auto-unseal tuve que desellarlo a mano).

kubectl scale sts ${vault_name} --replicas=5 -n <namespace>
kubectl wait --for=condition=Ready pod/${vault_name}-3 pod/${vault_name}-4 -n <namespace>
kubectl exec ${vault_name}-0 -n <namespace> -- env VAULT_TOKEN=<tu-token> vault operator raft list-peers
# Esperado: 5 peers, 1 líder, 4 followers

Después de remove-peer. El nodo ya ha sido expulsado del clúster. Si vuelves a subirlo con su PVC, arrancará con datos de Raft que el clúster ya no reconoce y no se unirá: te lo dirá en el log con el mismo node has been removed from the HA cluster. All vault data for this node must be cleaned up before it can be added back. Hay que borrar antes el PVC para que el pod empiece limpio y entre como peer nuevo por retry_join:

# Borra el PVC obsoleto de cada peer expulsado
kubectl delete pvc -n <namespace> data-${vault_name}-4
kubectl delete pvc -n <namespace> data-${vault_name}-3

# Sube de nuevo: arrancarán sin datos y se unirán solos
kubectl scale sts ${vault_name} --replicas=5 -n <namespace>
kubectl wait --for=condition=Ready pod/${vault_name}-3 pod/${vault_name}-4 -n <namespace>
kubectl exec ${vault_name}-0 -n <namespace> -- env VAULT_TOKEN=<tu-token> vault operator raft list-peers

En el clúster local los nodos entraron con un UUID nuevo, como followers votantes.

Todo esto da por hecho que tienes el auto-unseal configurado (en mi prueba local no lo tenía y tuve que desellar los pods a mano tras cada reinicio). Y si el quorum no se recupera por sí mismo, toca el procedimiento de emergencia con peers.json, que te cuento en Vault - Recuperar el quorum de Raft.

Para cerrar

Lo que me llevo es que "funciona" y "tiene margen" no son lo mismo. Con 3 pods vivos todo respondía, la API contestaba y no había ningún error, pero el clúster estaba a un paso de caerse y no lo sabía. list-peers me enseñaba 5 votantes sin decirme cuántos respondían, y ninguna señal de alarma. Fue autopilot state el que me dio la información clave (Healthy: false y Failure Tolerance: 0), y desde entonces es mi forma de comprobar el estado del clúster en cualquier operación de este tipo. Para cuando lo miré, ya había borrado la red de seguridad.

La regla que me quedo es sencilla: no destruyas lo que sobra hasta haber comprobado que el cambio que justificaba quitarlo funciona.

Si quieres ir más a fondo, esta es la documentación que he usado: