
En nuestra práctica, a menudo nos enfrentamos a la tarea de adaptar las aplicaciones cliente para que se ejecuten en Kubernetes. Al realizar estos trabajos, surgen una serie de problemas típicos. Recientemente cubrimos uno de ellos en el artículo
Archivos locales al transferir una aplicación a Kubernetes , y el otro, que ya está asociado con los procesos de CI / CD, se describirá en este artículo.
Comandos arbitrarios con Helm y werf
Una aplicación no es solo lógica y datos comerciales, sino también un conjunto de comandos arbitrarios que deben ejecutarse para una actualización exitosa. Estos pueden ser, por ejemplo, migraciones para bases de datos, "camareros" para la disponibilidad de recursos externos, algunos transcodificadores o desempacadores, registradores en Service Discovery externo: puede cumplir diferentes tareas en diferentes proyectos.
¿Qué ofrece Kubernetes para resolver tales problemas?
Kubernetes sabe cómo ejecutar contenedores como pods, por lo que la solución estándar es ejecutar un comando desde una imagen. Para esto, Kubernetes tiene una
primitiva Job que le permite ejecutar pod con contenedores de aplicaciones y supervisa la finalización de este pod.
Helm va un poco más allá y sugiere lanzar Job's en diferentes etapas del proceso de implementación. Estamos hablando de
ganchos Helm con los que puede ejecutar Job antes o después de actualizar los manifiestos de recursos. En nuestra experiencia, esta es una gran característica de Helm que se puede usar para resolver tareas de implementación.
Sin embargo, es imposible obtener información actualizada sobre el estado de los objetos durante el
lanzamiento en Helm, por lo tanto, utilizamos la utilidad
werf , que permite monitorear el estado de los recursos durante el lanzamiento directamente desde el sistema CI y, en caso de falla, diagnosticar el desglose más rápido.
Al final resultó que, estas características útiles Helm y werf a veces son mutuamente excluyentes, pero siempre hay una salida. Considere cómo puede monitorear el estado de los recursos y ejecutar comandos arbitrarios en el ejemplo de las migraciones.
Ejecución de migraciones antes del lanzamiento
Una parte integral del lanzamiento de cualquier aplicación de base de datos es actualizar el esquema de datos. La implementación estándar para aplicaciones que aplican migraciones ejecutando un comando separado implica los siguientes pasos:
- actualización de la base del código;
- inicio de la migración;
- cambiando el tráfico a la nueva versión de la aplicación.
Dentro de Kubernetes, el proceso debería ser el mismo, pero ajustado a lo que necesitamos:
- lanzar un contenedor con un nuevo código, que puede contener un nuevo conjunto de migraciones;
- inicie el proceso de aplicar migraciones en él, habiéndolo hecho antes de actualizar la versión de la aplicación
Considere la opción cuando
la base de datos para la aplicación ya se esté ejecutando y no necesitemos implementarla como parte del lanzamiento que implementa la aplicación. Dos ganchos son adecuados para aplicar migraciones:
pre-install
: funciona en la primera versión de Helm de la aplicación después de procesar todas las plantillas, pero antes de crear recursos en Kubernetes;pre-upgrade
: funciona al actualizar la versión de Helm y se ejecuta, como pre-install
, después de procesar las plantillas, pero antes de crear recursos en Kubernetes.
Ejemplo de trabajo usando Helm y los dos ganchos mencionados:
--- apiVersion: batch/v1 kind: Job metadata: name: {{ .Chart.Name }}-apply-migrations annotations: "helm.sh/hook": pre-install,pre-upgrade spec: activeDeadlineSeconds: 60 backoffLimit: 0 template: metadata: name: {{ .Chart.Name }}-apply-migrations spec: imagePullSecrets: - name: {{ required ".Values.registry.secret_name required" .Values.registry.secret_name }} containers: - name: job command: ["/usr/bin/php7.2", "artisan", "migrate", "--force"] {{ tuple "backend" . | include "werf_container_image" | indent 8 }} env: {{ tuple "backend" . | include "werf_container_env" | indent 8 }} - name: DB_HOST value: postgres restartPolicy: Never
Nota : la plantilla YAML anterior se creó teniendo en cuenta los detalles de werf. Para adaptarlo a un Helm "limpio", es suficiente:- reemplace
{{ tuple "backend" . | include "werf_container_image" | indent 8 }}
{{ tuple "backend" . | include "werf_container_image" | indent 8 }}
{{ tuple "backend" . | include "werf_container_image" | indent 8 }}
a la imagen del contenedor que necesita; - elimine la línea
{{ tuple "backend" . | include "werf_container_env" | indent 8 }}
{{ tuple "backend" . | include "werf_container_env" | indent 8 }}
{{ tuple "backend" . | include "werf_container_env" | indent 8 }}
, que se especifica en la clave env
.
Por lo tanto, esta plantilla Helm deberá agregarse al directorio
.helm/templates
, que ya contiene el resto de los recursos de la versión. Cuando
werf deploy --stages-storage :local
llama, todas las plantillas se procesarán primero y luego se cargarán en el clúster de Kubernetes.
Iniciando migraciones durante el proceso de lanzamiento
La opción anterior implica el uso de migraciones para el caso cuando la base de datos ya se está ejecutando. Pero, ¿qué sucede si necesitamos implementar la revisión de sucursal para la aplicación y la
base de datos se implementa con la aplicación en una versión?
NB : es posible que encuentre un problema similar al implementarse en el entorno de producción si utiliza el Servicio con un punto final que contiene la dirección IP de la base de datos para conectarse a la base de datos.En este caso, los
pre-install
y
pre-upgrade
no son adecuados para nosotros, ya que la aplicación intentará aplicar migraciones a la base de datos que
aún no existe . Por lo tanto, es necesario realizar migraciones
después del lanzamiento.
Cuando se utiliza Helm, esta tarea se puede lograr, ya que
no supervisa el estado de las aplicaciones. Después de cargar recursos en Kubernetes, los ganchos de publicación
siempre se activan:
post-install
: después de cargar todos los recursos en K8 en la primera versión;post-upgrade
: después de actualizar todos los recursos en K8 al actualizar la versión.
Sin embargo, como mencionamos anteriormente,
werf tiene un sistema de seguimiento de recursos durante el lanzamiento. Me detendré en esto un poco más en detalle:
- Para el seguimiento, werf utiliza las capacidades de la biblioteca kubedog , de la que ya hablamos en el blog.
- Esta característica en werf nos permite determinar de manera única el estado de la versión y mostrar información sobre la finalización exitosa o no exitosa de la implementación en la interfaz del sistema CI / CD.
- Sin recibir esta información, no se puede hablar de ninguna automatización del proceso de lanzamiento, ya que la creación exitosa de recursos en el clúster de Kubernetes es solo una de las etapas. Por ejemplo, es posible que la aplicación no se inicie debido a una configuración incorrecta o debido a un problema de red, pero para ver esto después de la
helm upgrade
, deberá realizar pasos adicionales.
Ahora volvamos a la aplicación de migraciones en Helm post-hook. Los problemas que encontramos:
- Muchas aplicaciones antes de iniciarse de una forma u otra verifican el estado del circuito en la base de datos. Por lo tanto, sin nuevas migraciones, la aplicación puede no iniciarse.
- Dado que werf, de forma predeterminada, garantiza que todos los objetos estén en estado
Ready
, los enlaces de publicación no funcionarán y las migraciones fallarán. - Los objetos de seguimiento se pueden deshabilitar mediante anotaciones adicionales, pero es imposible obtener información confiable sobre los resultados de la implementación.
Como resultado, llegamos a lo siguiente:
- Los trabajos se crean antes que los recursos principales, por lo que no hay necesidad de usar Helm Hooks para las migraciones .
- Sin embargo, se debe ejecutar un trabajo con migraciones en cada implementación. Para que esto suceda, Job debe tener un nombre único (aleatorio): en este caso, para Helm, esto es cada vez un nuevo objeto en la versión, que se creará en Kubernetes.
- Con tal lanzamiento, no tiene sentido preocuparse de que Job se acumule con las migraciones, ya que todas tendrán nombres únicos, y el Job anterior se eliminará con una nueva versión.
- Un trabajo con migraciones debe tener un contenedor init que verifique la disponibilidad de la base de datos; de lo contrario, tendremos una implementación descartada (Job caerá en el contenedor init).
La configuración resultante se parece a esto:
--- apiVersion: batch/v1 kind: Job metadata: name: {{ printf "%s-apply-migrations-%s" .Chart.Name (now | date "2006-01-02-15-04-05") }} spec: activeDeadlineSeconds: 60 backoffLimit: 0 template: metadata: name: {{ printf "%s-apply-migrations-%s" .Chart.Name (now | date "2006-01-02-15-04-05") }} spec: imagePullSecrets: - name: {{ required ".Values.registry.secret_name required" .Values.registry.secret_name }} initContainers: - name: wait-db image: alpine:3.6 ommand: ["/bin/sh", "-c", "while ! nc -z postgres 5432; do sleep 1; done;"] containers: - name: job command: ["/usr/bin/php7.2", "artisan", "migrate", "--force"] {{ tuple "backend" . | include "werf_container_image" | indent 8 }} env: {{ tuple "backend" . | include "werf_container_env" | indent 8 }} - name: DB_HOST value: postgres restartPolicy: Never
NB : Estrictamente hablando, los contenedores init para verificar la disponibilidad de la base de datos se usan mejor de todos modos.Un ejemplo de una plantilla universal para todas las operaciones de implementación.
Sin embargo, puede haber más operaciones que deben realizarse al momento del lanzamiento que el lanzamiento de las migraciones ya mencionadas. Puede controlar el orden de ejecución de Job no solo a través de los tipos de ganchos, sino también
asignando peso a cada uno de ellos , a través de la anotación
helm.sh/hook-weight
. Los ganchos se ordenan por peso en orden ascendente y, si el peso es el mismo, por nombre de recurso.
Con una gran cantidad de trabajos, es conveniente crear una plantilla universal para los trabajos y poner la configuración en
values.yaml
. Este último puede verse así:
deploy_jobs: - name: migrate command: '["/usr/bin/php7.2", "artisan", "migrate", "--force"]' activeDeadlineSeconds: 120 when: production: 'pre-install,pre-upgrade' staging: 'pre-install,pre-upgrade' _default: '' - name: cache-clear command: '["/usr/bin/php7.2", "artisan", "responsecache:clear"]' activeDeadlineSeconds: 60 when: _default: 'post-install,post-upgrade'
... y la plantilla en sí es así:
{{- range $index, $job := .Values.deploy_jobs }} --- apiVersion: batch/v1 kind: Job metadata: name: {{ $.Chart.Name }}-{{ $job.name }} annotations: "helm.sh/hook": {{ pluck $.Values.global.env $job.when | first | default $job.when._default }} "helm.sh/hook-weight": "1{{ $index }}" spec: activeDeadlineSeconds: {{ $job.activeDeadlineSeconds }} backoffLimit: 0 template: metadata: name: {{ $.Chart.Name }}-{{ $job.name }} spec: imagePullSecrets: - name: {{ required "$.Values.registry.secret_name required" $.Values.registry.secret_name }} initContainers: - name: wait-db image: alpine:3.6 ommand: ["/bin/sh", "-c", "while ! nc -z postgres 5432; do sleep 1; done;"] containers: - name: job command: {{ $job.command }} {{ tuple "backend" $ | include "werf_container_image" | indent 8 }} env: {{ tuple "backend" $ | include "werf_container_env" | indent 8 }} - name: DB_HOST value: postgres restartPolicy: Never {{- end }}
Este enfoque le permite agregar rápidamente nuevos comandos al proceso de lanzamiento y hace que la lista de comandos ejecutables sea más visual.
Conclusión
El artículo proporciona ejemplos de plantillas que le permiten describir operaciones comunes que necesita realizar en el proceso de lanzamiento de una nueva versión de la aplicación. Aunque fueron el resultado de la experiencia en la implementación de procesos de CI / CD en docenas de proyectos, no insistimos en que solo haya una solución adecuada para todas las tareas. Si los ejemplos descritos en el artículo no cubren las necesidades de su proyecto, estaremos encantados de ver situaciones en los comentarios que ayudarían a complementar este material.
Comentario de los desarrolladores de werf:
En el futuro, werf planea introducir etapas de implementación de recursos configurables por el usuario. Con la ayuda de tales etapas, será posible describir ambos casos y no solo.
PS
Lea también en nuestro blog: