Lesson  in  Kubernetes 101

StatefulSet

Cuando las réplicas no son intercambiables: identidad estable, un PersistentVolumeClaim propio por réplica y un Service headless que le da nombre DNS a cada Pod.

Todo lo que has desplegado con Deployments comparte una premisa silenciosa: las réplicas son intercambiables. Da igual qué Pod atienda la petición, da igual el orden en que arranquen, da igual cuál muera. Por eso sus nombres son sufijos aleatorios (web-7d4b9c8f6d-x2klp) y por eso todos comparten el mismo volumen o ninguno.

Una base de datos no funciona así. El nodo primario no es intercambiable con una réplica. El disco del db-0 contiene los datos del db-0 y de nadie más. Y el orden de arranque importa, porque el segundo nodo necesita saber a quién replicar.

El StatefulSet es el objeto que da esas tres garantías: identidad estable, almacenamiento propio por réplica y orden.

Trabaja desde la pestaña dev-machine.

Paso 1: El Service headless y el StatefulSet

Crea statefulset.yaml con las dos piezas, que son inseparables:

cat << 'EOF' > statefulset.yaml
apiVersion: v1
kind: Service
metadata:
  name: db
spec:
  clusterIP: None
  selector:
    app: db
  ports:
  - name: http
    port: 80
    targetPort: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
spec:
  serviceName: db
  replicas: 2
  selector:
    matchLabels:
      app: db
  template:
    metadata:
      labels:
        app: db
    spec:
      containers:
      - name: db
        image: ghcr.io/iximiuz/labs/nginx:alpine
        command: ["sh", "-c"]
        args:
        - |
          [ -f /data/identidad.txt ] || echo "$(hostname)-$(date +%s)" > /data/identidad.txt
          exec nginx -g 'daemon off;'
        ports:
        - containerPort: 80
        volumeMounts:
        - name: db
          mountPath: /data
  volumeClaimTemplates:
  - metadata:
      name: db
    spec:
      accessModes:
      - ReadWriteOnce
      storageClassName: local-path
      resources:
        requests:
          storage: 100Mi
EOF

El command escribe una identidad única en el disco solo si no existe ya. Ese fichero es la prueba del delito que usaremos al final.

El YAML, explicado con preguntas y respuestas

¿Qué es un Service con clusterIP: None?

Un Service headless: renuncia a la IP virtual y al balanceo. En vez de resolver un nombre a una IP única que reparte tráfico, el DNS devuelve las IPs de todos los Pods, y además le da a cada Pod su propio nombre: db-0.db.tienda.svc.cluster.local. Es justo lo contrario de lo que querías con un Deployment (dar igual con quién hablas) y justo lo que necesitas aquí (hablar con uno concreto).

¿Para qué sirve serviceName?

Le dice al StatefulSet qué Service headless gobierna el dominio DNS de sus Pods. Sin él no hay nombres estables por réplica. Es un campo que se olvida con frecuencia y cuyo síntoma es un DNS que no resuelve.

¿Qué es volumeClaimTemplates y en qué se diferencia de un volumes normal?

Es una fábrica de PVCs: por cada réplica, el StatefulSet crea un PersistentVolumeClaim propio siguiendo esta plantilla. El resultado son db-db-0 y db-db-1, cada uno con su volumen y sus datos. Compáralo con un Deployment cuyos Pods montaran el mismo PVC: ahí todos escribirían en el mismo disco, que es exactamente lo que una base de datos no quiere.

¿Los nombres de los Pods siguen siendo aleatorios?

No, y esa es la primera garantía: son ordinales y predecibles (db-0, db-1, db-2). Si db-0 cae, vuelve otro Pod llamado db-0, con el mismo nombre DNS y montando el mismo PVC. Un Deployment jamás te dará eso.

¿Qué garantiza el orden?

Que db-1 no arranca hasta que db-0 está Ready, y que al escalar hacia abajo se elimina primero el ordinal más alto. Se controla con podManagementPolicy: el valor por defecto OrderedReady es el que acabo de describir; Parallel lo desactiva cuando tu aplicación no lo necesita y quieres arranques rápidos.

Aplícalo y contempla el arranque, que es distinto a todo lo que has visto:

kubectl apply -f statefulset.yaml
kubectl get pods -l app=db --watch

Primero db-0, y solo cuando está Ready, db-1. Nada de los tres a la vez de un Deployment. Cuando terminen, mira la infraestructura que ha aparecido sola:

kubectl get statefulset,pods,pvc,pv -l app=db
kubectl get pvc

Dos Pods con nombre propio, dos PVCs con nombre derivado (db-db-0, db-db-1) y dos PersistentVolumes aprovisionados por local-path.

Paso 2: El DNS por réplica

Lanza un cliente y pregúntale al DNS por cada Pod, por su nombre:

kubectl run cliente --image=ghcr.io/iximiuz/labs/nginx:alpine --command -- sleep 100000
kubectl wait --for=condition=Ready pod/cliente --timeout=60s
kubectl exec cliente -- nslookup db-0.db.tienda.svc.cluster.local
kubectl exec cliente -- nslookup db.tienda.svc.cluster.local

La primera consulta devuelve la IP de un Pod concreto. La segunda, la de todos los Pods del Service (eso es un headless: sin IP virtual, el nombre del Service devuelve la lista completa). Compáralo con el web de la lección de Services, que devolvía una única IP virtual que balanceaba por detrás.

Esta es la pieza que hace posible un clúster de base de datos: cada nodo puede referirse a sus compañeros por un nombre que no cambia nunca, aunque los Pods se recreen y cambien de IP.

Paso 3: La prueba de la identidad

Anota lo que db-0 escribió en su disco:

kubectl exec db-0 -- cat /data/identidad.txt

Y ahora mátalo:

kubectl delete pod db-0
kubectl get pods -l app=db --watch

Observa con calma lo que ocurre. Vuelve un Pod que se llama otra vez db-0 (no db-9f8c7, como habría pasado con un Deployment), y en cuanto está en marcha:

kubectl exec db-0 -- cat /data/identidad.txt

El mismo contenido de antes. El Pod es nuevo (otro UID, otra IP), pero su identidad y su disco han sobrevivido: el StatefulSet volvió a montarle el PVC datos-db-0, que nunca llegó a borrarse. El command no reescribió el fichero porque ya existía.

Note

💡 Los PVC de un StatefulSet no se borran al escalar hacia abajo ni al eliminar el StatefulSet (salvo que configures persistentVolumeClaimRetentionPolicy). Es deliberado y a veces desconcertante: borras el StatefulSet, lo vuelves a crear, y aparecen los datos viejos. Prudencia por diseño, porque tirar datos de una base de datos por accidente es un error que no se deshace.

Resumen

  • StatefulSet cuando las réplicas no son intercambiables: bases de datos, colas, sistemas de consenso.
  • Tres garantías: identidad estable (db-0), un PVC propio por réplica (volumeClaimTemplates) y orden de arranque y parada.
  • El Service headless (clusterIP: None) da a cada Pod su nombre DNS; el serviceName los une.
  • Los PVC sobreviven al Pod, al escalado y hasta al propio StatefulSet.
  • Regla práctica: si dudas entre Deployment y StatefulSet, es un Deployment. El StatefulSet se elige cuando la aplicación te obliga.
Next lesson
DaemonSet