Organizar los pods en Kubernetes usando taints y tolerations

Hasta ahora, en los artículos anteriores, no he tenido en cuenta en qué nodos se despliegan mis aplicaciones. Hoy quiero contarte que existe la posibilidad de influir sobre cómo se organizan tus pods dentro de tu clúster a través de los taints y tolerations.

Taints

Lo cierto es que no sé cuál sería la traducción al español más acertada, pero me gusta pensar que en este caso taint es “olor” y los taints son los olores que desprende un nodo y hacen que los pods se alejen, a no ser que sean capaces de tolerarlos 😀 Por lo tanto, estos se asignan a los nodos y puedes saber cuáles tienen cada uno haciendo un describe de ellos:

#See the name of your nodes
kubectl get nodes
#Describe one of them
kubectl describe node aks-nodepool1-26643339-vmss000000

Dentro de la descripción, existe un apartado llamado Taints, que por ahora en mi nodo no tiene ningún valor:

➜ kubectl describe node aks-nodepool1-26643339-vmss000000
Name:               aks-nodepool1-26643339-vmss000000
Roles:              agent
Labels:             agentpool=nodepool1
                    beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/instance-type=Standard_DS2_v2
                    beta.kubernetes.io/os=linux
                    failure-domain.beta.kubernetes.io/region=northeurope
                    failure-domain.beta.kubernetes.io/zone=0
                    kubernetes.azure.com/cluster=MC_AKS-Demos_returngis_northeurope
                    kubernetes.azure.com/mode=system
                    kubernetes.azure.com/node-image-version=AKSUbuntu-1604-2020.06.10
                    kubernetes.azure.com/role=agent
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=aks-nodepool1-26643339-vmss000000
                    kubernetes.io/os=linux
                    kubernetes.io/role=agent
                    node-role.kubernetes.io/agent=
                    storageprofile=managed
                    storagetier=Premium_LRS
Annotations:        node.alpha.kubernetes.io/ttl: 0
                    volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp:  Tue, 23 Jun 2020 18:31:12 +0200
Taints:             <none>
[...]

Los taints están compuestos de una clave, un valor y un efecto representados como <key>=<value>:effect.

La clave y el valor pueden ser lo que tú quieras pero para el efecto hay tres tipos que puedes usar:

  • NoSchedule: evita que se programe el despliegue de pods que no toleren los taints que tienen este efecto.
  • PreferNoSchedule: es lo mismo que el anterior pero un poco más soft de cara al scheduler, que es el que tiene en cuenta este mecanismo.
  • NoExecute: este efecto es más agresivo ya que no solo restringe la ejecución en los nodos sino que si ya están desplegados en el mismo que acaba de obtener un nuevo taint con este efecto los reubica si no son capaces de tolerarlo.

Para ver todo esto con ejemplos, la forma de asociar un taint a un nodo es la siguiente:

kubectl taint node aks-nodepool1-26643339-vmss000000 type=production:NoSchedule

Si ahora vuelves a hacer un describe de este nodo comprobarás que este tiene un nuevo taint asociado:

➜ kubectl describe node aks-nodepool1-26643339-vmss000000
Name:               aks-nodepool1-26643339-vmss000000
Roles:              agent
Labels:             agentpool=nodepool1
                    beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/instance-type=Standard_DS2_v2
                    beta.kubernetes.io/os=linux
                    failure-domain.beta.kubernetes.io/region=northeurope
                    failure-domain.beta.kubernetes.io/zone=0
                    kubernetes.azure.com/cluster=MC_AKS-Demos_returngis_northeurope
                    kubernetes.azure.com/mode=system
                    kubernetes.azure.com/node-image-version=AKSUbuntu-1604-2020.06.10
                    kubernetes.azure.com/role=agent
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=aks-nodepool1-26643339-vmss000000
                    kubernetes.io/os=linux
                    kubernetes.io/role=agent
                    node-role.kubernetes.io/agent=
                    storageprofile=managed
                    storagetier=Premium_LRS
Annotations:        node.alpha.kubernetes.io/ttl: 0
                    volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp:  Tue, 23 Jun 2020 18:31:12 +0200
Taints:             type=production:NoSchedule
[...]

Puedes poner múltiples taints en el mismo nodo o el mismo taint a todos los nodos utilizando el mismo comando pero con nodes y –all:

Si estás trabajando con AKS, otra de las cosas que puedes hacer es, en el momento de añadir un nodepool, que no es más que un conjunto de máquinas de un mismo tipo y versión, asociarle los taints a los nodos que lo componen durante la creación:

az aks nodepool add \
    --resource-group myResourceGroup \
    --cluster-name myAKSCluster \
    --name taintnp \
    --node-count 1 \
    --node-taints sku=gpu:NoSchedule \
    --no-wait

Tolerations

Desde el punto de vista de los pods digamos que estos son capaces de tolerar los taints. ¿Esto qué significa? Pues básicamente que estos pueden tener en su definición cuáles de los supuestos taints podrían aceptar para que el scheduler pueda o no valorar un nodo en concreto para su despliegue.

Para ver algunos ejemplos de tolerations puedes echar un vistazo a alguno de los pods dentro del namespace kube-system.

kubectl get po -n kube-system

Al igual que en los nodos, haz un describe de uno del tipo kube-proxy-* para ver sus tolerancias:

kubectl describe pod kube-proxy-4rqhx -n kube-system

Verás que en la parte inferior de la descripción aparecen todas las definidas para este tipo de pod:

Desplegar pod de prueba

Para terminar vamos a hacer algunas pruebas con unos pods desde cero. Para ello lanza el siguiente despliegue:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3  
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx
          ports:
            - containerPort: 80
      tolerations:
        - key: type
          operator: Equal #this is the default value
          value: production
          effect: NoSchedule

Como puedes ver, al final he incluido un apartado llamado tolerations, donde básicamente puedes indicar qué claves con qué valor y qué efecto tolerarán los pods que se creen a raíz de este deployment. Cuando despliegues el mismo en tu clúster te darás cuenta de que las tres réplicas estarán repartidas entre los tres nodos de la misma forma que siempre:

➜ kubectl get po -o wide
NAME                                                              READY   STATUS    RESTARTS   AGE   IP           NODE                                NOMINATED NODE   READINESS GATES
nginx-deployment-6ffd9989d-bhsl8                                  1/1     Running   0          13s   10.244.2.4   aks-nodepool1-26643339-vmss000002   <none>           <none>
nginx-deployment-6ffd9989d-dbvr9                                  1/1     Running   0          13s   10.244.0.9   aks-nodepool1-26643339-vmss000000   <none>           <none>
nginx-deployment-6ffd9989d-hgdfp                                  1/1     Running   0          13s   10.244.1.5   aks-nodepool1-26643339-vmss000001   <none>           <none>

¿Entonces? Esto es así porque aunque un pod tolere ciertos taints esto no significa que no pueda desplegarse en otros nodos que no tengan estas restricciones, ya que por ahora los otros dos nodos no tienen ninguna.

Para comprobar esto vamos a utilizar otro taint en el segundo nodo que además utilice el efecto NoExecute, para que si hay algún pod que no lo tolere sea expulsado del nodo.

kubectl taint node aks-nodepool1-26643339-vmss000001 type=dev:NoExecute

Si ahora compruebas el estado de tus pods, verás que el que se encontraba en este nodo está siendo reubicado en otro:

➜  Kubelet kubectl get pod -o wide
NAME                                                              READY   STATUS              RESTARTS   AGE   IP           NODE                                NOMINATED NODE   READINESS GATES
nginx-deployment-6ffd9989d-268jb                                  0/1     ContainerCreating   0          5s    <none>       aks-nodepool1-26643339-vmss000002   <none>           <none>
nginx-deployment-6ffd9989d-bhsl8                                  1/1     Running             0          17m   10.244.2.4   aks-nodepool1-26643339-vmss000002   <none>           <none>
nginx-deployment-6ffd9989d-dbvr9                                  1/1     Running             0          17m   10.244.0.9   aks-nodepool1-26643339-vmss000000   <none>           <none>
nginx-deployment-6ffd9989d-hgdfp                                  0/1     Terminating         0          17m   10.244.1.5   aks-nodepool1-26643339-vmss000001   <none>           <none>

La última prueba sería que el tercer nodo tuviera un taint diferente, de tal manera que a los pods no les quedara más remedio que ubicarse todos en el restante, que sería el cero, el cual si toleran.

kubectl taint node aks-nodepool1-26643339-vmss000002 type=dev:NoSchedule

En este caso, verás que los pods que ya estaban desplegados siguen estando en el nodo aks-nodepool1-26643339-vmss000002:

➜ kubectl get pods -o wide                                                
NAME                                                              READY   STATUS    RESTARTS   AGE     IP           NODE                                NOMINATED NODE   READINESS GATES
nginx-deployment-6ffd9989d-268jb                                  1/1     Running   0          4h15m   10.244.2.7   aks-nodepool1-26643339-vmss000002   <none>           <none>
nginx-deployment-6ffd9989d-bhsl8                                  1/1     Running   0          4h32m   10.244.2.4   aks-nodepool1-26643339-vmss000002   <none>           <none>
nginx-deployment-6ffd9989d-dbvr9                                  1/1     Running   0          4h32m   10.244.0.9   aks-nodepool1-26643339-vmss000000   <none>           <none>

Esto es así porque este cambio no afecta a los ya desplegados. Para este ejemplo voy a eliminar todos los pods y que el scheduler vuelva a desplegarlos donde corresponda:

kubectl delete po --all

Si vuelves a comprobar tus pods verás que ahora todos están alojados en el nodo cero.

➜ kubectl get pods -o wide 
NAME                                                              READY   STATUS    RESTARTS   AGE   IP            NODE                                NOMINATED NODE   READINESS GATES
nginx-deployment-6ffd9989d-bksfw                                  1/1     Running   0          51s   10.244.0.11   aks-nodepool1-26643339-vmss000000   <none>           <none>
nginx-deployment-6ffd9989d-mfjwr                                  1/1     Running   0          51s   10.244.0.10   aks-nodepool1-26643339-vmss000000   <none>           <none>
nginx-deployment-6ffd9989d-mfphr                                  1/1     Running   0          50s   10.244.0.12   aks-nodepool1-26643339-vmss000000   <none>           <none>

Si quisieras eliminar un taint de un nodo el comando sería el mismo con un menos al final:

kubectl taint node aks-nodepool1-26643339-vmss000002 type=dev:NoSchedule-

Como ves, gracias a los taints en los nodos y los tolerations en los pods puedes influir en el despliegue de estos de una forma mucho más lógica, por hardware, capacidades, equipos, lo que quieras 🙂

¡Saludos!