Aplicaciones con estado en Kubernetes con StatefulSets

Gisela Torres
Computación en la nube
Siempre que puedas desarrollar aplicaciones sin estado, que no dependan ni deban conocer al resto de réplicas

Aplicaciones con estado en Kubernetes con StatefulSets

Siempre que puedas desarrollar aplicaciones sin estado, que no dependan ni deban conocer al resto de réplicas, te va a permitir tener un desarrollo altamente escalable y ágil. Sin embargo esto no es siempre posible. Existen algunos tipos donde cada réplica no solo necesita de su propio almacenamiento, sino que su identidad sea estable en el tiempo. También es importante mantener un orden y ser conscientes del resto de integrantes/pods. En este artículo quiero contarte cómo desplegar aplicaciones con estado en Kubernetes, usando el recurso StatefulSet y un clúster de MongoDB como ejemplo.

StatefulSet

Para poder trabajar en Kubernetes con este tipo de aplicaciones necesitamos utilizar un recurso llamado StatefulSet. Estos se caracterízan, a diferencia de los Deployments, por proveer a nuestras aplicaciones de las siguientes capacidades:

  • Almacenamiento persistente e independiente para cada uno de los pods.
  • Identificador predecible y estable en el tiempo.
  • Despliegue, escalado y actualizaciones de manera ordenada entre las réplicas.

Para verlo con un ejemplo, voy a desplegar un clúster de MongoDB, que seguro que te aclarará cada uno de los puntos. Debes tener en cuenta que para poder trabajar con StatefulSets necesitarás antes tener disponibles los siguientes recursos:

Crear un servicio headless

Antes de crear el recurso StatefulSet necesitas un servicio del tipo headless. Este se encargará de gestionar las IPs de los pods creados por el recurso, a través del selector que se establezca, y será capaz de asociar cada pod con la IP correspondiente.

apiVersion: v1

kind: Service

metadata:

  name: mongodb-svc

  labels:

    app: db

    name: mongodb

spec:

  clusterIP: None

  selector:

    app: db

    name: mongodb

  ports:

  - port: 27017

    targetPort: 27017

Como puedes ver, para que un servicio sea headless simplemente hay que poner la propiedad clusterIP a None.

Al crearlo, comprobarás que el servicio no tiene asociada ninguna IP interna, ya que no es necesario que balancee entre los pods:

Giselas-iMac:statefulset gis$ kubectl get svc

NAME          TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)     AGE

kubernetes    ClusterIP   10.0.0.1     <none>        443/TCP     23m

mongodb-svc   ClusterIP   None         <none>        27017/TCP   5s

Por ahora tampoco tiene ningún endpoint asociado, como era de esperar:

Giselas-iMac:statefulset gis$ kubectl describe svc mongodb-svc

Name:              mongodb-svc

Namespace:         default

Labels:            app=db

                   name=mongodb

Annotations:       kubectl.kubernetes.io/last-applied-configuration:

                     {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"labels":{"app":"db","name":"mongodb"},"name":"mongodb-svc","namespace":"...

Selector:          app=db,name=mongodb

Type:              ClusterIP

IP:                None

Port:              <unset>  27017/TCP

TargetPort:        27017/TCP

Endpoints:         <none>

Session Affinity:  None

Events:            <none>

Crear un StorageClass

Si estás trabajando con Azure Kubernetes Service como yo, ya tienes estas storage classes creadas por defecto:

Giselas-iMac:statefulset gis$ kubectl get storageclass

NAME                PROVISIONER                AGE

azurefile           kubernetes.io/azure-file   4d17h

azurefile-premium   kubernetes.io/azure-file   4d17h

default (default)   kubernetes.io/azure-disk   4d17h

managed-premium     kubernetes.io/azure-disk   4d17h

Sin embargo, para hacer el ejemplo de principio a fin, voy a crear un StorageClass específico para este escenario:

apiVersion: storage.k8s.io/v1

kind: StorageClass

metadata:

  name: storage-mongo

provisioner: kubernetes.io/azure-disk

reclaimPolicy: Retain

parameters:

  kind: Managed

  storageaccounttype: StandardSSD_LRS

En este caso, he elegido el tipo Azure Disk, ya que los volúmenes que se generen solo serán asociados a un único pod, y como tipo he utilizado el Standard SSD con redundancia local.

Crear el recurso StatefulSet

Ahora que ya tienes el servicio headless y una storage class, ya puedes definir el recurso Statefulset con la configuración de MongoDB en formato clúster o replica set:

apiVersion: apps/v1

kind: StatefulSet

metadata:

  name: mongo

spec:

  selector:

    matchLabels:

      app: db

      name: mongodb

  serviceName: mongodb-svc #the name of the headless service

  replicas: 3

  template:

    metadata:

      labels:

        app: db

        name: mongodb

    spec:

      terminationGracePeriodSeconds: 10 #This is important for databases

      containers:

      - name: mongo

        image: mongo:3.6

        command: #https://docs.mongodb.com/manual/tutorial/deploy-replica-set/#start-each-member-of-the-replica-set-with-the-appropriate-options

          - mongod

        args:

          - --bind_ip=0.0.0.0

          - --replSet=rs0 #The name of the replica set that the mongod is part of. All hosts in the replica set must have the same set name.

          - --dbpath=/data/db

        livenessProbe:

            exec:

              command:

                - mongo

                - --eval

                - "db.adminCommand('ping')"

        ports:

        - containerPort: 27017

        volumeMounts:

        - name: mongo-storage

          mountPath: /data/db

  volumeClaimTemplates:

    - metadata:

        name: mongo-storage

      spec:

        storageClassName: storage-mongo

        accessModes: ["ReadWriteOnce"]

        resources:

          requests:

            storage: 100Gi

Como puedes ver, la definición del StatefulSet necesita el nombre del servicio headless que creaste (serviceName), el número de replicas, una plantilla que defina los pods y otra plantilla para los volúmenes que se deben generar por cada una de las réplicas, en la cual se indica la storage class creada anteriormente. En el caso de MongoDB, hay que configurarlo en modo replica set, y es por ello que necesitas el apartado command y args (más información aquí).

Crea este recurso y lista los StatefulSets dentro de tu clúster.

Giselas-iMac:statefulset gis$ kubectl apply -f statefulset.yaml

statefulset.apps/mongo created

Giselas-iMac:statefulset gis$ kubectl get statefulset

NAME    READY   AGE

mongo   0/3     8s

Como ves, está a la espera de las tres réplicas. También puedes consultar los pods para ver cómo va el progreso:

Giselas-iMac:statefulset gis$ kubectl get po

NAME      READY   STATUS    RESTARTS   AGE

mongo-0   1/1     Running   0          2m39s

mongo-1   0/1     Pending   0          8s

Lo primero que llama la atención, si lo comparamos con los Deployments, es que primer pod se ha creado con el nombre del StatetfulSet seguido de -0, y hasta que este no ha comenzado a ejecutarse el siguiente pod no ha aparecido en listado para su creación, y así hasta cumplir con el número de réplicas elegidas:

Giselas-iMac:statefulset gis$ kubectl get pods

NAME      READY   STATUS    RESTARTS   AGE

mongo-0   1/1     Running   0          2m50s

mongo-1   1/1     Running   0          2m10s

mongo-2   1/1     Running   0          92s

Esta es una de las características que comenté al principio de este artículo: el despliegue se realiza de manera ordenada, no se utilizan nombres aleatorios sino que siguen un indice. Es por ello que el proceso será mucho más lento.

Lo siguiente que puedes comprobar es si, tal y como se definió en el StatefulSet, se han creado los volúmenes persistentes para cada uno de los pods con la storage class creada:

Giselas-iMac:statefulset gis$ kubectl get pv

NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                           STORAGECLASS    REASON   AGE

pvc-8bf21368-9650-47a0-bdaf-2380abb5d4e0   100Gi      RWO            Retain           Bound    default/mongo-storage-mongo-2   storage-mongo            9m39s

pvc-bc620a6f-7ab5-4678-b172-edb960422f19   100Gi      RWO            Retain           Bound    default/mongo-storage-mongo-1   storage-mongo            10m

pvc-e2119a57-bb46-4406-91ec-74f811ee832a   100Gi      RWO            Retain           Bound    default/mongo-storage-mongo-0   storage-mongo            22m

Por último, si vuelves a comprobar los endpoints asociados al servicio verás que ahora contendrá tres IPs consecutivas:

Giselas-iMac:statefulset gis$ kubectl describe endpoints mongodb-svc

Name:         mongodb-svc

Namespace:    default

Labels:       app=db

              name=mongodb

Annotations:  endpoints.kubernetes.io/last-change-trigger-time: 2020-05-02T23:04:35Z

Subsets:

  Addresses:          10.244.0.64,10.244.0.65,10.244.0.66

  NotReadyAddresses:  <none>

  Ports:

    Name     Port   Protocol

    ----     ----   --------

    <unset>  27017  TCP


Events:  <none>

Inicializar el replica set de MongoDB

Ahora ya tienes tus tres réplicas funcionando, pero todavía no están trabajando conjuntamente como un clúster. Para ello es necesario inicializar el mismo desde dentro de uno de los pods:

#Connect mongo-0

kubectl exec -it mongo-0 mongo

Una vez dentro debes lanzar rs.initiate con los FQDN de cada uno de los pods de la siguiente manera:

#Initiate the replicaset

rs.initiate({_id: "rs0", version: 1, members: [

  { _id: 0, host : "mongo-0.mongodb-svc.default.svc.cluster.local:27017" },

  { _id: 1, host : "mongo-1.mongodb-svc.default.svc.cluster.local:27017" },

  { _id: 2, host : "mongo-2.mongodb-svc.default.svc.cluster.local:27017" }

]});

Al hacer esto deberías de obtener un mensaje como el siguiente:

> rs.initiate({_id: "rs0", version: 1, members: [

...   { _id: 0, host : "mongo-0.mongodb-svc.default.svc.cluster.local:27017" },

...   { _id: 1, host : "mongo-1.mongodb-svc.default.svc.cluster.local:27017" },

...   { _id: 2, host : "mongo-2.mongodb-svc.default.svc.cluster.local:27017" }

... ]});

{

        "ok" : 1,

        "operationTime" : Timestamp(1588460857, 1),

        "$clusterTime" : {

                "clusterTime" : Timestamp(1588460857, 1),

                "signature" : {

                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),

                        "keyId" : NumberLong(0)

                }

        }

}

rs0:SECONDARY>

Lo cual significa que ya tienes todos tus pods trabajando de manera conjunta. Para confirmar la configuración puedes lanzar por último este comando:

rs.conf()

Con él recuperarás todos los miembros de tu clúster y el host asociado a cada uno de ellos, el cual gracias al StatefulSet sabemos que no cambiará, incluso si algo le pasa a los pods.

Nota: Este proceso de inicialización se suele automatizar, sobre todo pensando en si queremos escalar de manera automática el clúster de MongoDB. Sin embargo, en este artículo quería que vieras cómo es el proceso manual y cómo se hace uso de las identidades estáticas de los pods.

Probar el clúster de MongoDB en Kubernetes

Ahora que ya tienes tu clúster funcionando con tres réplicas, las cuales se han ido creando de manera secuencial, tienen una identidad que sabemos que no va a cambiar, así como su propio almacenamiento, lo último que nos queda por comprobar es que funciona correctamente. Para ello voy a crear un despligue con Mongo Express:

apiVersion: apps/v1

kind: Deployment

metadata:

  name: mongo-express

  labels:

    app: web

    name: mongo-express

spec:

  replicas: 2

  selector:

    matchLabels:

      app: web

      name: mongo-express

  template:

    metadata:

      labels:

        app: web

        name: mongo-express

    spec:

      containers:

      - name: mongo-express

        image: mongo-express

        env:

        - name: ME_CONFIG_MONGODB_SERVER

          value: mongodb://mongo-0.mongodb-svc,mongo-1.mongodb-svc,mongo-2.mongodb-svc?replicaSet=ns0

        ports:

          - containerPort: 8081


---


apiVersion: v1

kind: Service

metadata:

  name: mongo-express-svc

spec:

  type: LoadBalancer

  selector:

    app: web

    name: mongo-express

  ports:

  - protocol: TCP

    port: 80

    targetPort: 8081

Para indicar que tenemos un replica set, he modificado el valor de la variable ME_CONFIG_MONGODB_SERVER utilizando una cadena de conexión que especifica todos los miembros del clúster, una vez más haciendo uso de los identificadores que StatefulSet te proporciona, garantizándote de que estos no cambiarán. También he definido un servicio del tipo LoadBalancer para que puedas acceder a la web desde fuera de Kubernetes. Revisa la IP del servicio mongo-express para acceder al mismo.

Mongo Express en Kubernetes

Crea una base de datos y añade si quieres algún documento.

Después, conéctate a uno de los nodos secundarios y comprueba que el contenido se ha replicado satisfactoriamente a través de los siguientes comandos:

#Check mongo-1

kubectl exec -it mongo-1 mongo

rs.slaveOk()

show dbs

use returngis

db.people.find()

En mi caso la salida es la siguiente:

rs0:SECONDARY> rs.slaveOk()

rs0:SECONDARY> show dbs

admin      0.000GB

config     0.000GB

local      0.000GB

returngis  0.000GB

rs0:SECONDARY> use returngis

switched to db returngis

rs0:SECONDARY> db.people.find()

{ "_id" : ObjectId("5eae0254ff30720007a49f09"), "firsName" : "Gisela", "lastName" : "Torres", "age" : 35, "groups" : [ "bike", "rowing" ] }

Otra de las pruebas que puedes hacer es que mongo-0, que es el primario dentro del clúster de Mongo, falle (eliminándolo por ejemplo).

kubectl delete pod mongo-0

Comprobarás que uno de los otros dos nodos se vuelve el primario, el eliminado vuelve a recrearse con el mismo nombre, asociado al mismo storage y volverá a pertenecer al replica set de Mongo (cuando se recupere) ya que su identidad no ha cambiado.

El código del ejemplo lo tienes en mi GitHub.

¡Saludos!



Gisela Torres

Gisela Torres trabaja en Microsoft como Cloud Solution Architect. Se trata de un puesto técnico cuya misión es apoyar y asesorar sobre soluciones y arquitecturas cloud utilizando Microsoft Azure como plataforma. Antes de eso trabajo como arquitecta de software y desarrolladora de aplicaciones en varias empresas. Durante esos años recibio varios premios por ejemplo Most Valuable Professional en Microsoft Azure. Le encanta programar y la tecnología en general.

Más artículos de Gisela en su blog - https://www.returngis.net/

Related Posts

Únete a nuestra Newsletter

Lidera la Conversación en la Nube