Cómo trabajar con el objeto Deployment en Kubernetes

En uno de los primeros artículos que escribí sobre Kubernetes te dije, sin entrar en detalle, que la forma recomendada de desplegar los pods sobre tu clúster era a través del objeto Deployment, debido a que este gestiona los objetos ReplicaSet por nosotros y nos ayuda no solo en el despliegue sino también es posteriores actualizaciones. En este artículo quiero contarte todo lo que deberías saber sobre este objeto.

Cómo funciona un Deployment

Los deployments se definen como objetos de alto nivel que te ayudan no solo en el despliegue de tus aplicaciones en Kubernetes sino que también te permiten hacer actualizaciones de forma declarativa. En versiones anteriores, esta tarea se realizaba gestionando directamente el objeto ReplicationController, de forma más manual, pero ahora se ha delegado en estos y los ReplicaSets, de los que ya te hablé en un articulo anterior.

Como siempre, vamos a verlo con un ejemplo que siempre se entiende mejor 🙂 . En este caso voy a desplegar un servidor web con tres replicas, inicialmente de Nginx:

#web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-deployment
  labels:
    app: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:latest          
          ports:
            - name: http
              containerPort: 80

---

apiVersion: v1
kind: Service
metadata:
  name: web-service
spec:
  type: LoadBalancer
  selector:
    app: web
  ports:
    - protocol: TCP
      port: 80
      targetPort: http

Además del Deployment voy a crear un objeto de tipo Service para poder acceder al servidor web desde fuera del clúster, y ver de forma sencilla lo que va ocurriendo con él mismo durante el artículo.
Por lo tanto, cuando despliegues este manifiesto tendrás 6 objetos nuevos en tu entorno: un Deployment, un ReplicaSet, tres pods y un servicio de tipo LoadBalancer.

kubectl apply -f web-deployment.yaml --record

En el comando anterior he añadido el parámetro –record, el cual verás para qué sirve más adelante 😉

En esta primera fase la salida esperada será la siguiente:

deployment.apps/web-deployment created
service/web-service created

Puedes comprobar todos los objetos creados a través del siguiente comando:

kubectl get deploy,rs,po,svc
#or kubectl get deployments,replicasets,pods,services

y el resultado será parecido a este:

NAME                                   READY   UP-TO-DATE   AVAILABLE   AGE
deployment.extensions/web-deployment   3/3     3            3           70s

NAME                                              DESIRED   CURRENT   READY   AGE
replicaset.extensions/web-deployment-6646b5b476   3         3         3       70s

NAME                                  READY   STATUS    RESTARTS   AGE
pod/web-deployment-6646b5b476-6f6sz   1/1     Running   0          70s
pod/web-deployment-6646b5b476-m9hjh   1/1     Running   0          70s
pod/web-deployment-6646b5b476-mglhs   1/1     Running   0          70s

NAME                  TYPE           CLUSTER-IP    EXTERNAL-IP    PORT(S)        AGE
service/kubernetes    ClusterIP      10.0.0.1      <none>         443/TCP        70s
service/web-service   LoadBalancer   10.0.82.126   52.155.177.3   80:31717/TCP   70s

En mi caso, si accedo a través del navegador a la IP 52.155.177.3 veré el servidor de Nginx funcionando.

Ahora bien, cuando se dice que gracias a los deployments tenemos una actualización de manera declarativa ¿esto qué significa? Pues básicamente que podemos hacer modificaciones en el YAML que define el despliegue, ya sea volviendo a enviar todo él o a través de kubectl patch/edit, y el controlador de los deployments determinará si dicho cambio conlleva una actualización de los pods (esto es, cuando se ha modificado la sección template del pod dentro del objeto Deployment). Para comprobar esto, cambia la imagen nginx utilizada en la creación por mcr.microsoft.com/dotnet/core/samples:aspnetapp:

#web-deployment.v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-deployment
  labels:
    app: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          #image: nginx:latest
          image: mcr.microsoft.com/dotnet/core/samples:aspnetapp
          ports:
            - name: http
              containerPort: 80

---

apiVersion: v1
kind: Service
metadata:
  name: web-service
spec:
  type: LoadBalancer
  selector:
    app: web
  ports:
    - protocol: TCP
      port: 80
      targetPort: http

y aplica los nuevos cambios:

kubectl apply -f web-deployment.v2.yaml --record

Al tratarse del mismo objeto Deployment creado anteriormente, el resultado ya no será created sino configured esta vez:

deployment.apps/web-deployment configured
service/web-service configured

Como es la segunda vez que mandas información acerca del mismo Deployment, si vuelves a comprobar el listado de ReplicaSets comprobarás que ahora tienes dos para el mismo despliegue:

$kubectl get rs

NAME                        DESIRED   CURRENT   READY   AGE
web-deployment-5b5f4f975    3         3         3       92s
web-deployment-6646b5b476   0         0         0       4m24s

Pero sigues teniendo el mismo número de pods:

$kubectl get po

NAME                             READY   STATUS    RESTARTS   AGE
web-deployment-5b5f4f975-kvh9k   1/1     Running   0          2m3s
web-deployment-5b5f4f975-mflx9   1/1     Running   0          2m5s
web-deployment-5b5f4f975-r8c9p   1/1     Running   0          2m7s

En el artículo sobre los ReplicaSet expliqué que estos crean los pods con el nombre del ReplicaSet (el cual está compuesto por el nombre del despliegue más un literal aleatorio) y un literal aleatorio por cada pod. Si comparas el nombre que tenían tus pods la primera vez y el nombre que tienen ahora podemos deducir que el ReplicaSet que está gestionando los pods no es el mismo que al principio, sino el nuevo que vemos con el literal 5b5f4f975. Esto es así porque el Deployment utiliza un nuevo ReplicaSet cada vez que haces una actualización, el cual tiene la versión actualizada de la plantilla que tiene que utilizar para los pods.

¿Pero por qué se queda el anterior con cero pods? Muy sencillo: en el caso de que necesitemos dar marcha atrás, porque la actualización no esté funcionando correctamente por ejemplo, el Deployment volverá a utilizar dicho ReplicaSet para recrear los pods con la configuración anterior. Puedes comprobarlo utilizando el comando rollout undo:

kubectl rollout undo deployment web-deployment

Kubernetes responderá con el siguiente mensaje:

deployment.extensions/web-deployment rolled back

y podrás ver que vuelves a tener un Nginx funcionando, en lugar de la aplicación de ejemplo de ASP.NET Core. De hecho, si vuelves a revisar los ReplicaSets comprobarás que ahora el que controla los pods es, de nuevo, el primero que se generó:

$kubectl get rs

NAME                        DESIRED   CURRENT   READY   AGE
web-deployment-5b5f4f975    0         0         0       10m
web-deployment-6646b5b476   3         3         3       13m

Llegados a este punto es posible que te preguntes ¿entonces voy a tener tantos ReplicaSet como todas las actualizaciones que esta aplicación haya tenido en su vida? La respuesta es no 🙂 Por defecto sólo se guardan las 10 últimas revisiones, aunque es configurable a través de revisionHistoryLimit, dentro del apartado spec del deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-deployment
  labels:
    app: web
spec:
  revisionHistoryLimit: 5
  replicas: 3
  selector:
[...]

Por otro lado, si quisieras ir a una revisión en concreto podrías hacerlo añadiendo al comando el parámetro –to-revision=N, siempre dentro de las revisiones disponibles:

kubectl rollout undo deployment web-deployment --to-revision=2

Otro dato importante es poder ver el estado en el que se encuentra una actualización. En este ejemplo los cambios son demasiado rápidos, por lo que voy a ralentizar el tiempo que tardan los pods en estar listos.

#Slow down
kubectl patch deployment web-deployment -p '{"spec": {"minReadySeconds" : 5}}'

El resultado de esta acción será el siguiente:

deployment.extensions/web-deployment patched

Nota: Este cambio en el deployment no hará que se actualicen los pods, al estar fuera del apartado template.

Ahora voy a actualizar de nuevo el objeto Deployment, pero esta vez a través de set image:

#Update the image
kubectl set image deployment web-deployment nginx=httpd:latest --record

El resultado será que la imagen ha sido actualizada:

deployment.extensions/web-deployment image updated

En este caso, sí generará un redespliegue de pods con la nueva imagen. Para conocer el estado de esta actualización puedes utilizar rollout status:

#See the status of the rollout
kubectl rollout status deployment web-deployment

Este último se quedará a la espera hasta que finalice la actualización, mostrando el avance de la misma:

Waiting for deployment "web-deployment" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "web-deployment" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "web-deployment" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "web-deployment" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "web-deployment" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "web-deployment" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "web-deployment" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "web-deployment" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "web-deployment" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "web-deployment" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "web-deployment" rollout to finish: 1 old replicas are pending termination...
deployment "web-deployment" successfully rolled out

Una vez que termine el proceso, comprobarás que ahora lo que tienes es un servidor Apache ejecutándose en tus pods 🙂

Para ver el histórico de las últimas actualizaciones que has llevado a cabo sobre tu despliegue inicial utiliza el comando rollout history:

kubectl rollout history deployment web-deployment

El resultado debería de ser como el que sigue:

REVISION  CHANGE-CAUSE
3         kubectl apply --filename=web-deployment.yaml --record=true
4         kubectl apply --filename=web-deployment.v2.yaml --record=true
5         kubectl set image deployment web-deployment nginx=httpd:latest --record=true

Gracias a el parámetro –record puedes ver qué comando se lanzó para este deployment cada vez que se actualizó, de tal manera que será más sencillo poder determinar a qué versión tienes que ir si fuera necesario. Sin embargo, lo habitual es que utilicemos el mismo nombre de archivo, si estamos trabajando con un despliegue automatizado, pero este tendrá diferentes versiones del contenido. Si es el caso, podemos ver el detalle de cada actualización indicando la revisión:

kubectl rollout history deployment web-deployment --revision=4

De hecho, en la misma podrás ver que solo se muestra el contenido de la plantilla a utilizar para los pods:

deployment.extensions/web-deployment with revision #4
Pod Template:
  Labels:       app=web
        pod-template-hash=5b5f4f975
  Annotations:  kubernetes.io/change-cause: kubectl apply --filename=web-deployment.v2.yaml --record=true
  Containers:
   nginx:
    Image:      mcr.microsoft.com/dotnet/core/samples:aspnetapp
    Port:       80/TCP
    Host Port:  0/TCP
    Environment:        <none>
    Mounts:     <none>
  Volumes:      <none>

Diferentes estrategias de despliegue

Por último, es importante saber que los Deployments nos permiten llevar a cabo la actualización a través de dos estrategias. Por defecto se utiliza la llamada RollingUpdate la cual va remplazando los pods, evitando la pérdida de servicio, basándose en los valores maxSurge (cuantos pods puede haber por encima del valor deseado) y maxUnavailable (cuántos como máximo pueden no estar disponibles). Por defecto, ambas propiedades tienen un valor del 25%, aunque se puede especificar un número exacto en lugar de un porcentaje. Debes tener en cuenta que para poder utilizar esta estrategia tu aplicación debe de ser capaz de convivir con versiones diferentes.
Por otro lado, también es posible utilizar el modo Recreate que lo que haces es eliminar primero todos los pods antiguos y después crea los nuevos. En este caso sí que hay pérdida de servicio.

Esta configuración se hace a través del apartado strategy:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-deployment
  labels:
    app: web
spec:
  revisionHistoryLimit: 5
  strategy:
    type: Recreate

[...]

Escalar un deployment

Por último, si solo quieres aumentar o disminuir el número de réplicas puedes hacerlo a través de scale:

kubectl scale deployment web-deployment --replicas=50

En un próximo artículo te mostraré cómo hacer esto de manera automática.

¡Saludos!