Kubernetes: aventuras y desventuras de parchear (kubectl patch).

Kubernetes es una potente herramienta de orquestación de contenedores donde se ejecutan muchos y diferentes objetos que en algún momento nos va a interesar modificar.

Para ello Kubernetes nos ofrece un interesante mecanismo: patch que vamos a explicar y veremos que está lejos de ser una herramienta lo bastante potente como sería deseable.

Operaciones de parcheo (sustitución) en Kubernetes

Según la documentación de Kubernetes y las normas de la API de Kubernetes se definen tres tipos (con –type):

Strategic

Es el tipo de parche que usa Kubernetes por defecto y es un tipo nativo, definido en el SIG, sigue la estructura del objeto original pero indicando los cambios (por defecto unir: merge, por eso se conoce por strategic merge patch) en un fichero yaml. Por ejemplo si tenemos el siguiente servicio (en el fichero service.yml):

apiVersion: v1
kind: Service
metadata:
  labels:
    app: traefik
  name: traefik
  namespace: kube-system
spec:
  clusterIP: 10.43.122.171
  externalTrafficPolicy: Cluster
  ports:
  - name: http
    nodePort: 30200
    port: 80
    protocol: TCP
    targetPort: http
  - name: https
    nodePort: 31832
    port: 443
    protocol: TCP
    targetPort: https
  selector:
    app: traefik
  type: LoadBalancer

Vamos a utilizar el comando kubectl patch -f service.yml --type="strategic" -p "$(cat patch.yml)" --dry-run -o yaml que nos va a permitir realizar pruebas sobre los objetos sin el peligro de modificar su contenido en el cluster Kubernetes.

Si queremos que dicho servicio escuche por un puerto adicional utilizaremos la estrategia “merge” y aplicaremos el siguiente parche (patch.yml):

spec:
  ports:
  - name: dashboard
    port: 8080
    protocol: TCP
    targetPort: dashboard

Como vemos, el parche solo sigue el objeto servicio hasta donde queremos realizar el cambio (el array “ports”) y al ser un cambio de tipo “strategic merge” se dedicará a añadirlo a la lista como se ve en el volcado del comando:

...
spec:
  clusterIP: 10.43.122.171
  externalTrafficPolicy: Cluster
  ports:
  - name: dashboard
    port: 8080
    protocol: TCP
    targetPort: dashboard
  - name: http
    nodePort: 30200
    port: 80
    protocol: TCP
    targetPort: http
...

Pero si en vez de “merge” utilizamos “replace” lo que hacemos es eliminar todo el contenido del subárbol donde estamos al indicar la etiqueta “$patch: replace” y en su lugar pone directamente el contenido del parche. Por ejemplo para cambiar el contenido del array usamos como “patch.yml”:

spec:
  ports:
  - $patch: replace
  - name: dashboard
    port: 8080
    protocol: TCP
    targetPort: dashboard

En este ejemplo se elimina todo el contenido de “ports:” y en su lugar se deja el objeto definido después de la etiqueta “$patch: replace”, aunque el orden no es importante, la etiqueta puede ir detrás y tiene el mismo efecto. El resultado del anterior es:

...
  ports:
  - name: dashboard
    port: 8080
    protocol: TCP
    targetPort: dashboard
  selector:
...

Por último “delete” que se indica con “$patch: delete” elimina el contenido del subárbol, aunque se le añada contenido este no es añadido.

spec:
  $patch: delete

El resultado será el contenido de spec vacío:

apiVersion: v1
kind: Service
metadata:
  labels:
    app: traefik
  name: traefik
  namespace: kube-system
spec: {}

Merge

Este tipo de parche es un cambio radical frente a “strategic” ya que requiere la utilización de JSON Merge Patch (RFC7386), se puede aplicar como yaml o json y se basa en un procedimiento de aplicación de cambios con a partir de una pareja objetivo y contenido.

Si el objetivo existe y el valor no es “null” lo sustituye, si el valor es “null” elimina el objetivo. Si el objetivo no existe lo crea. El resto del contenido se mantiene.

Para ver claramente las diferencias si aplicamos el siguiente parche al fichero service.yml

spec:
  ports:
  - name: dashboard
    port: 8080
    protocol: TCP
    targetPort: dashboard

Con kubectl patch -f service.yml --type="merge" -p "$(cat patch.yml)" --dry-run -o yaml obtendremos el mismo resultado que con el tipo “stategic replace”, ya que el objeto apuntado (spec.ports) se sustituye por el valor introducido.

Para eliminar un objetivo (Por ejemplo eliminar el selector) usaremos kubectl patch -f service.yml --type="merge" -p '{"spec": {"selector": null}}' --dry-run -o yaml

Y para modificar el selector también podemos usar un json como kubectl patch -f service.yml --type="merge" -p '{"spec": {"selector": "newtraefik"}}' --dry-run -o yaml

JSON

Los dos tipos de parches/sustituciones anteriores son interesantes para aplicaciones sencillas como modificar un nombre, cambiar o eliminar una etiqueta o añadir un chequeo a un despliegue sin editarlo.

Pero si necesitamos trabajar con listas, encontramos los mismos muy limitados y ademas muchos objetos no están bien preparados para ser capaces de entender dichos parches (por ejemplo el objeto Ingress sufre de dicho problema) y en vez de modificar las listas siempre reemplaza.

Por eso existe este tercer tipo de parche más eficiente que los dos anteriores, ya que dispone de más operaciones (“add”, “remove”, “replace”, “move”, “copy”, o “test”), y de un apuntador más preciso al objeto que queremos modificar (definido con JavaScript Object Notation (JSON) Pointer RFC6901).

  • “JSON Pointer”

Es la cadena de objetos separados por “/” apuntando al objeto que queremos manipular. Los caracteres “/” y “~” se deben codificar como “~0” y “~1”.

Es especialmente interesante para los arrays, ya que puede hacer referencia a un elemento de un array indicando su posición con un número o indicar un elemento no existente con “-“. A diferencia de JSONPath no es capaz de seleccionar un elemento de un Array usando una búsqueda, por lo que aunque este método es una mejor sigue teniendo importantes limitaciones.

El parche tendrá el formato: ‘{“op”:”OPERACIÓN”,”PARÁMETRO1″,”VALOR1″,…}, habitualmente tendrá al menos dos componentes: operación “op” y ruta “path”, las operaciones con este tipo de parche se exponen a continuación:

  • Añadir
    • Parámetro operación: “add”
    • Parámetro ruta: “path”
    • Parámetro valor: “value”

Si el objeto apuntado por ruta existe lo reemplaza, si no existe lo crea y si es un Array lo crea en la posición indicada del Array, por lo que para añadir elementos a un Array se usa en el apuntador el elemento “-“.

  • Eliminar
    • Parámetro operación: “remove”
    • Parámetro ruta: “path”

Si el objeto apuntado por la ruta existe lo elimina, junto con la rama correspondiente.

  • Reemplazar
    • Parámetro operación: “replace”
    • Parámetro ruta: “path”
    • Parámetro valor: “value”

Si el objeto apuntado por al ruta existe lo sustituye por el indicado en valor.

  • Mover
    • Parámetro operación: “move”
    • Parámetro ruta origen: “from”
    • Parámetro ruta destino: “path”

Si ambas rutas existen, procederá a copiar el objeto apuntado por la ruta origen a la ruta destino y eliminará el que exista en la ruta origen.

  • Copiar
    • Parámetro operación: “copy”
    • Parámetro ruta origen: “from”
    • Parámetro ruta destino: “path”

Si ambas rutas existen, procederá a copiar el objeto apuntado por la ruta origen a la ruta destino.

  • Comprobar
    • Parámetro operación: “test”
    • Parámetro ruta: “path”
    • Parámetro valor: “value”

Compara el objeto pasado como valor con el que existe en la ruta, si la ruta no existe o el objeto es diferente devolverá un error.

Aplicando un parche JSON a un objeto Ingress de Kubernetes

Para entenderlo mejor iremos tomando como ejemplo un elemento Ingress cuya función principal es funcionar como proxy inverso desde el exterior hacia los servicios que corren en la plataforma.

Kubernetes: aventuras y desventuras de parchear (kubectl patch). 1
Los servicios web siempre interesa publicarlos al exterior a través de un Ingress

En el gráfico vemos un Ingress arriba del todo, funcionando como un proxy inverso necesita básicamente conocer tres parámetros: el dominio por el que se publica, la ruta por la que publica y que servicio publicamos.

Kubernetes: aventuras y desventuras de parchear (kubectl patch). 2

Este Ingress tendrá un yaml como el siguiente:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: traefik
  namespace: external-dns
spec:
  rules:
  - host: powerdns-admin.disasterproject.com
    http:
      paths:
      - backend:
          serviceName: powerdns-admin
          servicePort: 80
        path: /

Como podemos ver el formato de especificaciones de un Ingress incluye al menos dos listas, una de servidores y otra de rutas de servicio. Vamos a ver que este tipo de objetos incluso con el parche tipo “json” son de una manipulación compleja.

Podemos usar “add” para crear nuevos “host”, por ejemplo un nuevo servicio para la powerdns-api:

kubectl -n external-dns patch ingress traefik --type "json" -p '[{"op":"add","path":"/spec/rules/-","value":{"host":"powerdns-api.disasterproject.com","http":{"paths":[{"backend":{"serviceName":"powerdns-api","servicePort":8081 },"path":"/"}]} } }]'

Como podemos ver, añadimos un nuevo host al final de las reglas con el resultado:

Kubernetes: aventuras y desventuras de parchear (kubectl patch). 3

Ahora tenemos dos elementos en el Array de reglas y dentro un Array de backends y tenemos un importante problema porque JSON Pointer no es capaz de buscar y solo se le puede indicar un objeto en un Array por su orden (0,1,2,3,etc). La única solución actualmente es hacer una búsqueda con jsonpath y numerar los resultados, por ejemplo con:

$ kubectl -n external-dns get ingress/traefik -o jsonpath='{.spec.rules[*].host}'|tr ' ' '\n'|cat --number
     1  powerdns-admin.disasterproject.com
     2  powerdns-api.disasterproject.com

Si queremos modificar la ruta de powerdns-api “/” a “/api” ya tenemos el número de orden para el JSONPointer (el número de orden en el array empieza por 0, por lo que debemos restar uno):

$ kubectl -n external-dns patch ingress/traefik --type "json" -p '[{"op":"replace","path":"/spec/rules/1/http/paths/0/path","value":"/api" }]'

$ kubectl -n external-dns get ingress/traefik -o yaml
...
  - host: powerdns-api.disasterproject.com
    http:
      paths:
      - backend:
          serviceName: powerdns-api
          servicePort: 8081
        path: /api
...

¿Pero y si solo quiero modificar el dominio donde está sirviendo la ruta “/api”? JSON Pointer muestra importantes limitaciones en este punto, ya que tenemos que ir mirando (seguramente con scripts) por cada array el orden que ocupa en el mismo. Claramente le falta la potencia de jsonpath que permite hacer consultas como la siguiente que nos va a devolver el backend del dominio powedns-api.disasterproject.com que tenga un path “/api”:

$ kubectl get -n external-dns ingress traefik -o jsonpath='{.spec.rules[?(@.host=="powerdns-api.disasterproject.com")].http.paths[?(@.path=="/api")]}'

map[backend:map[serviceName:powerdns-api servicePort:8081] path:/api]

En este jsonpath vemos dos buquedas @.host==”powerdns-api.disasterproject.com” y @.path==”/api”, no disponibles en ninguna de las herramienta de parcheo. Lo mas avanzado que dispone JSON patch es la operación test, pero adolece de los problemas de JSON Pointer:

$ kubectl -n external-dns patch ingress traefik --type="json" -p '[{"op": "test","path": "/metadata/name","value": "traefik2"}]' --dry-run -o yaml
error: Testing value /metadata/name failed

Y por lo tanto es ineficiente en scripts, dejando como única solución jsonpath para listar todos los objetos, filtrar los resultados, coger el indice obtenido e ir realizando de nuevo la operación para todos los arrays que posea el objeto que vamos a parchear.

Referencias

Todas las pruebas se han realizado en el entorno de pruebas definido en https://www.disasterproject.com/index.php/2019/07/12/entorno-minimo-para-demos-de-kubernetes/ y en https://www.disasterproject.com/index.php/2019/07/18/kubernetes-mas-simple-k3s/

Volver arriba