Fusión 3 vías en werf: despliegue en Kubernetes con Helm "potenciado"

Ocurrió lo que nosotros (y no solo nosotros) hemos estado esperando durante mucho tiempo: werf, nuestra herramienta Open Source para construir aplicaciones y entregarlas en Kubernetes, ahora soporta la aplicación de cambios mediante parches de fusión de 3 vías. Además, existe la posibilidad de adoptar recursos K8s existentes en los lanzamientos de Helm sin recrear esos recursos.

Fusión 3 vías en werf: despliegue en Kubernetes con Helm "potenciado"

En resumen, establecemos WERF_THREE_WAY_MERGE=enabled — obtenemos un despliegue “como en kubectl apply", compatible con instalaciones existentes en Helm 2 y un poco más.

Pero comencemos desde la teoría: ¿qué son exactamente los parches de fusión de 3 vías, cómo se llegó al enfoque de su generación y por qué son importantes en los procesos CI/CD con infraestructura basada en Kubernetes? Después de eso, veremos qué es realmente la fusión de 3 vías en werf, qué modos se utilizan por defecto y cómo gestionar esto.

¿Qué es un parche de fusión de 3 vías?

Así que comencemos con la tarea de implementar recursos descritos en manifiestos YAML en Kubernetes.

Para trabajar con los recursos, Kubernetes API ofrece las siguientes operaciones básicas: crear, parchear, reemplazar y eliminar. Se supone que con su ayuda se debe construir un despliegue continuo conveniente de recursos en el clúster. ¿Cómo?

Comandos imperativos de kubectl

El primer enfoque para administrar objetos en Kubernetes es utilizar comandos imperativos de kubectl para crear, modificar y eliminar esos objetos. En términos simples:

  • con el comando kubectl run puede iniciar un Deployment o Job:
    kubectl run --generator=deployment/apps.v1 NOMBRE_DEL_DEPLOYMENT --image=IMAGEN
  • con el comando kubectl scale — cambiar el número de réplicas:
    kubectl scale --replicas=3 deployment/mysql
  • etc.

Este enfoque puede parecer conveniente a primera vista. Sin embargo, hay problemas:

  1. Es difícil automatizar.
  2. ¿Cómo reflejar la configuración en Git? ¿Cómo hacer la revisión de los cambios que ocurren en el clúster?
  3. ¿Cómo garantizar la reproducibilidad de la configuración al reiniciar?
  4. …

Es evidente que este enfoque no se combina bien con el almacenamiento junto con el código de la aplicación y la infraestructura como código (IaC; o incluso GitOps como una opción más moderna que está ganando popularidad en el ecosistema de Kubernetes). Por lo tanto, estos comandos en kubectl no recibieron más desarrollo.

Operaciones crear, obtener, reemplazar y eliminar

Con la creación inicial todo es simple: enviamos el manifiesto a la operación create en kube api y el recurso se crea. La representación en YAML del manifiesto se puede almacenar en Git, y para crearla se puede usar el comando kubectl create -f manifest.yaml para la eliminación.

Con también es simple: utilizamos el mismo manifest.yaml manifest.yaml de Git a la estructura kubectl delete -f manifest.yaml.

Operación replace permite reemplazar completamente la configuración del recurso por uno nuevo, sin necesidad de recrear el recurso. Esto significa que, antes de realizar un cambio en el recurso, es lógico solicitar la versión actual mediante la operación get, modificarla y actualizarla con la operación replace. En kube apiserver se integra optimistic locking y, si después de la operación get el objeto ha cambiado, la operación replace no se llevará a cabo.

Para almacenar la configuración en Git y actualizar con replace, es necesario realizar la operación get, fusionar la configuración desde Git con lo que hemos obtenido y ejecutar replace. Por defecto, kubectl solo permite usar el comando kubectl replace -f manifest.yaml, donde manifest.yaml —ya completado (en nuestro caso, fusionado) el manifiesto, que es necesario implementar. Eso significa que el usuario debe realizar la fusión de manifiestos, lo cual no es trivial…

También cabe destacar que, aunque manifest.yaml se almacena en Git, no podemos saber de antemano si debemos crear o actualizar el objeto; eso debe hacerlo el software del usuario.

Total: ¿Podemos construir un despliegue continuo solo con create, replace y delete, asegurando el almacenamiento de la configuración de infraestructura en Git junto con el código y proporcionando un CI/CD conveniente?

En principio, podemos… Para ello, será necesario implementar una operación de fusión de manifiestos y algún tipo de envoltura que:

  • verifique la existencia del objeto en el clúster,
  • realice la creación inicial del recurso,
  • lo actualice o lo elimine.

Al actualizar, debemos tener en cuenta que el recurso podría haber cambiado desde la última vez get y manejar automáticamente el caso de optimistic locking, realizando intentos de actualización.

Sin embargo, ¿por qué reinventar la rueda, cuando kube-apiserver ofrece otra forma de actualizar recursos: la operación patch, que alivia al usuario de parte de los problemas descritos?

Patch

Aquí estamos, hablando de parches.

Los parches son la principal forma de aplicar cambios a objetos existentes en Kubernetes. La operación patch funciona de manera que:

  • al usuario de kube-apiserver se le requiere enviar un parche en formato JSON e indicar el objeto,
  • y el apiserver se encargará de interpretar el estado actual del objeto y llevarlo a la forma requerida.

Optimistic locking no es necesario en este caso. Esta operación es más declarativa en comparación con replace, aunque inicialmente puede parecer lo contrario.

Así que:

  • con la operación en kube api y el recurso se crea. La representación en YAML del manifiesto se puede almacenar en Git, y para crearla se puede usar el comando creamos un objeto a partir del manifiesto en Git,
  • usando delete —eliminamos si el objeto ya no es necesario,
  • usando patch — modificamos el objeto llevándolo a la forma descrita en Git.

Sin embargo, para hacer esto, es necesario crear el parche correcto!

Cómo funcionan los parches en Helm 2: fusión bidireccional

Al instalar por primera vez una versión, Helm realiza la operación en kube api y el recurso se crea. La representación en YAML del manifiesto se puede almacenar en Git, y para crearla se puede usar el comando para los recursos del chart.

Al actualizar la versión, Helm para cada recurso:

  • calcula el parche entre la versión del recurso del chart anterior y la versión actual del chart,
  • aplica este parche.

A este parche lo llamaremos parche de fusión bidireccional, porque en su creación participan 2 manifiestos:

  • el manifiesto del recurso de la versión anterior,
  • el manifiesto del recurso de la versión actual.

Al eliminar, la operación delete en kube apiserver se llama para los recursos que fueron declarados en la versión anterior, pero no en la actual.

El enfoque con el parche de fusión bidireccional tiene un problema: lleva a una desincronización entre el estado real del recurso en el clúster y el manifiesto en Git.

Ilustración del problema con el ejemplo

  • En Git, en el chart se almacena un manifiesto, en el que el campo image en el Deployment tiene el valor ubuntu:18.04.
  • El usuario a través de kubectl edit cambió el valor de este campo a ubuntu:19.04.
  • Al redeplegar el chart de Helm, no se genera un parche, porque el campo image en la versión anterior de la versión y en el chart actual son iguales.
  • Después del redepliegue, image permanece ubuntu:19.04, aunque en el chart está escrito ubuntu:18.04.

Hemos tenido desincronización y hemos perdido la declaratividad.

¿Qué es un recurso sincronizado?

En general, la plena coherencia entre el manifiesto del recurso en el clúster en funcionamiento y el manifiesto en Git es imposible de alcanzar. Porque en el manifiesto real pueden existir anotaciones/etiquetas administrativas, contenedores adicionales y otros datos que se agregan y eliminan dinámicamente del recurso por ciertos controladores. Estos datos no podemos y no queremos mantener en Git. Sin embargo, queremos que al desplegar, los campos que hemos especificado claramente en Git tomen los valores correspondientes.

Se obtiene una regla general para un recurso sincronizado: al desplegar un recurso, solo se pueden modificar o eliminar aquellos campos que están claramente indicados en el manifiesto de Git (o que estaban indicados en una versión anterior y ahora han sido eliminados).

parche de fusión tridireccional

La idea principal parche de fusión tridireccional: se genera un parche entre la última versión aplicada del manifiesto de Git y la versión objetivo del manifiesto de Git teniendo en cuenta la versión actual del manifiesto del clúster en funcionamiento. El parche final debe cumplir con la regla del recurso sincronizado:

  • Los nuevos campos añadidos a la versión de destino se incluyen mediante un parche;
  • Los campos que ya existían en la última versión aplicada y no existen en la de destino se anulan mediante un parche;
  • Los campos en la versión actual del objeto que difieren de la versión de destino del manifiesto se actualizan mediante un parche.

Así es como se generan los parches kubectl apply:

  • La última versión aplicada del manifiesto se guarda en la anotación del propio objeto,
  • la de destino se toma del archivo YAML especificado,
  • la actual se toma del clúster en funcionamiento.

Ahora que hemos aclarado la teoría, es hora de contar lo que hemos hecho en werf.

Aplicación de cambios en werf

Anteriormente, werf, al igual que Helm 2, utilizaba parches de fusión bidireccional.

Parche de reparación

Para pasar al nuevo tipo de parches, el parche de fusión tridireccional, el primer paso que hemos dado es introducir lo que se llama parches de reparación.

Al desplegar se utiliza el parche estándar de fusión bidireccional, pero werf genera adicionalmente un parche que sincroniza el estado real del recurso con lo que está escrito en Git (se crea dicho parche utilizando la misma regla de recurso sincronizado descrita anteriormente).

En caso de que ocurra una desincronización, al final del despliegue el usuario recibe una ADVERTENCIA con el mensaje correspondiente y un parche que debe aplicarse para llevar el recurso a un estado sincronizado. Además, este parche se registra en una anotación especial werf.io/repair-patch. Se supone que el usuario manualmente mismo aplicará este parche: werf no lo aplicará por principio.

La generación de parches de reparación es una medida temporal que permite probar la creación de parches según el principio de fusión tridireccional, pero no aplicar automáticamente estos parches. Actualmente, este modo de operación está habilitado por defecto.

Parche de fusión tridireccional solo para nuevas versiones

A partir del 1 de diciembre de 2019, las versiones beta y alfa de werf comenzarán por defecto a utilizar parches de fusión tridireccional completos para aplicar cambios solo para nuevas versiones de Helm lanzadas a través de werf. Las versiones ya existentes continuarán utilizando el enfoque con parches de fusión bidireccional + parches de reparación.

Este modo de operación se puede habilitar explícitamente configurando WERF_THREE_WAY_MERGE_MODE=onlyNewReleases ya ahora.

Nota: la funcionalidad ha aparecido en werf a lo largo de varias versiones: en el canal alfa se volvió estable a partir de la versión v1.0.5-alpha.19, y en el canal beta — desde v1.0.4-beta.20.

parche de fusión tridireccional para todas las versiones

A partir del 15 de diciembre de 2019, las versiones beta y alpha de werf comenzarán a utilizar por defecto parches de fusión de 3 vías completos para aplicar cambios en todas las versiones.

Este modo de operación se puede habilitar explícitamente configurando WERF_THREE_WAY_MERGE_MODE=enabled ya ahora.

¿Cómo manejar la escalabilidad automática de recursos?

En Kubernetes existen 2 tipos de escalabilidad automática: HPA (horizontal) y VPA (vertical).

El horizontal selecciona automáticamente la cantidad de réplicas, el vertical — la cantidad de recursos. Tanto el número de réplicas como los requisitos de recursos se especifican en el manifiesto del recurso (ver spec.replicas o spec.containers[].resources.limits.cpu, spec.containers[].resources.limits.memory y otros).

Problema: si el usuario configura un recurso en el chart de tal manera que se especifiquen valores específicos para los recursos o réplicas y se habiliten los autoscalers para ese recurso, entonces en cada despliegue werf restablecerá esos valores a lo que está escrito en el manifiesto del chart.

Hay dos soluciones para el problema. Para empezar, lo mejor es evitar especificar valores escalables automáticamente en el manifiesto del chart. Sin embargo, si esta opción no es viable por alguna razón (por ejemplo, porque en el chart es conveniente establecer limitaciones iniciales de recursos y cantidad de réplicas), werf ofrece las siguientes anotaciones:

  • werf.io/set-replicas-only-on-creation=true
  • werf.io/set-resources-only-on-creation=true

Con esta anotación, werf no restablecerá los valores correspondientes en cada despliegue, sino que solo los establecerá en la creación inicial del recurso.

Para más detalles, consulte la documentación del proyecto sobre HPA y VPA.

Prohibir el uso de parches de fusión de 3 vías

Por ahora, el usuario puede prohibir el uso de nuevos parches en werf a través de la variable de entorno WERF_THREE_WAY_MERGE_MODE=disabled. Sin embargo, a partir del 1 de marzo de 2020, esta prohibición dejará de ser efectiva y solo se podrán utilizar parches de fusión de 3 vías.

Adopción de recursos en werf

Dominar el método de aplicación de cambios mediante parches de fusión de 3 vías nos permitió implementar de inmediato una función como la adopción de recursos existentes en el clúster en un lanzamiento de Helm.

Helm 2 tiene un problema: no se puede agregar un recurso que ya existe en el clúster a los manifiestos del chart sin recrear ese recurso desde cero (ver #6031, #3275). Hemos enseñado a werf a aceptar recursos existentes en un lanzamiento. Para esto, se debe establecer en la versión actual del recurso del clúster en funcionamiento una anotación (por ejemplo, mediante kubectl edit):

"werf.io/allow-adoption-by-release": RELEASE_NAME

Ahora es necesario describir el recurso en el chart y, al hacer el siguiente despliegue del lanzamiento mediante werf con el nombre correspondiente, el recurso existente será aceptado en este lanzamiento y permanecerá bajo su gestión. Además, en el proceso de aceptación del recurso en el lanzamiento, werf llevará el estado actual del recurso del clúster en funcionamiento al estado descrito en el chart, utilizando los mismos parches de fusión 3-way y la regla de recurso sincronizado.

Nota: configuración WERF_THREE_WAY_MERGE_MODE no afecta a la adopción de recursos; en caso de adopción, siempre se utiliza un parche de fusión 3-way.

Los detalles están en la documentación.

Conclusiones y planes futuros

Espero que después de este artículo quede más claro qué son los parches de fusión 3-way y por qué se adoptaron. Desde un punto de vista práctico del desarrollo del proyecto werf, su implementación ha sido un paso más hacia la mejora del despliegue similar a Helm. Ahora podemos olvidarnos de los problemas de sincronización de configuración que a menudo surgían al usar Helm 2. Al mismo tiempo, se ha añadido una nueva característica útil sobre la adopción de recursos de Kubernetes que ya estaban descargados en el lanzamiento de Helm.

En el despliegue similar a Helm siguen existiendo algunos problemas y dificultades, como el uso de plantillas de Go, y continuaremos abordándolos.

La información sobre los métodos de actualización de recursos y adopción también se puede encontrar en esta página de documentación.

Helm 3

Vale la pena mencionar el lanzamiento hace apenas unos días de la nueva versión mayor de Helm — v3, — que también utiliza parches de fusión 3-way y elimina a Tiller. La nueva versión de Helm requiere migración de las instalaciones existentes para convertirlas al nuevo formato de almacenamiento de lanzamientos.

Por su parte, werf ya se ha deshecho del uso de Tiller, ha cambiado a 3-way-merge y ha añadido muchas otras cosas, manteniéndose compatible con las instalaciones ya existentes en Helm 2 (no es necesario ejecutar scripts de migración). Por lo tanto, mientras werf no se haya cambiado a Helm 3, los usuarios de werf no pierden las principales ventajas de Helm 3 sobre Helm 2 (también están presentes en werf).

Sin embargo, el cambio de werf a la base de código de Helm 3 es inevitable y ocurrirá en un futuro cercano. Se presume que será en werf 1.1 o werf 1.2 (en este momento, la versión principal de werf es 1.0; más detalles sobre el esquema de versionado de werf se pueden encontrar en aquí). Durante este tiempo, Helm 3 se estabilizará.

P.D.

También puedes leer en nuestro blog:

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster