werf: nuestra herramienta para CI/CD en Kubernetes (resumen e informe en video)

27 de mayo en el salón principal de la conferencia DevOpsConf 2019, celebrada en el marco del festival RIT++ 2019, como parte de la sección "Entrega continua", se entregó un informe "werf - nuestra herramienta para CI/CD en Kubernetes". habla de esos Problemas y desafíos que todos enfrentan al implementar Kubernetes., así como sobre matices que pueden no ser perceptibles de inmediato. Analizando posibles soluciones, mostramos cómo se implementa esto en una herramienta Open Source. patio.

Desde la presentación, nuestra utilidad (anteriormente conocida como dapp) ha alcanzado un hito histórico de 1000 estrellas en GitHub – esperamos que su creciente comunidad de usuarios haga la vida más fácil a muchos ingenieros de DevOps.

werf: nuestra herramienta para CI/CD en Kubernetes (resumen e informe en video)

Entonces, te presentamos vídeo del informe (~47 minutos, mucho más informativo que el artículo) y el extracto principal en forma de texto. ¡Ir!

Entregando código a Kubernetes

La charla ya no será sobre werf, sino sobre CI/CD en Kubernetes, implicando que nuestro software está empaquetado en contenedores Docker. (Hablé de esto en informe 2016), y K8 se utilizarán para ejecutarlo en producción. (más sobre esto en 2017 años).

¿Cómo se ve la entrega en Kubernetes?

  • Hay un repositorio Git con el código y las instrucciones para construirlo. La aplicación está integrada en una imagen de Docker y se publica en el Registro de Docker.
  • El mismo repositorio también contiene instrucciones sobre cómo implementar y ejecutar la aplicación. En la etapa de implementación, estas instrucciones se envían a Kubernetes, que recibe la imagen deseada del registro y la inicia.
  • Además, suele haber pruebas. Algunas de estas se pueden hacer al publicar una imagen. También puede (siguiendo las mismas instrucciones) implementar una copia de la aplicación (en un espacio de nombres K8 separado o en un clúster separado) y ejecutar pruebas allí.
  • Finalmente, necesita un sistema de CI que reciba eventos de Git (o clics en botones) y llame a todas las etapas designadas: compilación, publicación, implementación y prueba.

werf: nuestra herramienta para CI/CD en Kubernetes (resumen e informe en video)

Hay algunas notas importantes aquí:

  1. Porque tenemos una infraestructura inmutable. (infraestructura inmutable), la imagen de la aplicación que se utiliza en todas las etapas (puesta en escena, producción, etc.), debe haber uno. Hablé de esto con más detalle y con ejemplos. aquí.
  2. Porque seguimos la infraestructura como enfoque de código. (IAC), el código de la aplicación, las instrucciones para ensamblarla y ejecutarla deben estar exactamente en un repositorio. Para obtener más información sobre esto, consulte el mismo informe.
  3. cadena de entrega (entrega) normalmente lo vemos así: la aplicación fue ensamblada, probada y lanzada (etapa de lanzamiento) y eso es todo: la entrega se ha realizado. Pero en realidad, el usuario obtiene lo que usted implementó. no luego, cuando lo entregaste a producción, y cuando pudo ir allí y esta producción funcionó. Entonces creo que la cadena de entrega termina. sólo en la etapa operativa (correr), o más precisamente, incluso en el momento en que el código fue eliminado de producción (reemplazándolo por uno nuevo).

Volvamos al esquema de entrega anterior en Kubernetes: lo inventamos no solo nosotros, sino literalmente todos los que se ocuparon de este problema. De hecho, este patrón ahora se llama GitOps. (puedes leer más sobre el término y las ideas detrás de él) aquí). Veamos las etapas del esquema.

etapa de construcción

Parecería que podemos hablar de crear imágenes de Docker en 2019, cuando todo el mundo sepa cómo escribir Dockerfiles y ejecutarlos. docker build?.. Estos son los matices a los que me gustaría prestar atención:

  1. Peso de la imagen importa, así que usa multi-etapadejar en la imagen sólo la aplicación que realmente sea necesaria para la operación.
  2. Número de capas debe minimizarse combinando cadenas de RUN-comandos según significado.
  3. Sin embargo, esto añade problemas depuración, porque cuando el ensamblaje falla, debes encontrar el comando correcto de la cadena que causó el problema.
  4. Velocidad de montaje importante porque queremos implementar cambios rápidamente y ver los resultados. Por ejemplo, no desea reconstruir las dependencias en las bibliotecas de idiomas cada vez que crea una aplicación.
  5. A menudo, necesita un repositorio de Git muchas imagenes, que se puede resolver mediante un conjunto de Dockerfiles (o etapas con nombre en un archivo) y un script Bash con su ensamblaje secuencial.

Esto fue sólo la punta del iceberg al que todos nos enfrentamos. Pero existen otros problemas, en particular:

  1. A menudo, en la etapa de montaje necesitamos algo. montar (por ejemplo, almacenar en caché el resultado de un comando como apt en un directorio de terceros).
  2. Queremos Ansible en lugar de escribir en shell.
  3. Queremos construir sin Docker (¿Por qué necesitamos una máquina virtual adicional en la que necesitamos configurar todo para esto, cuando ya tenemos un clúster de Kubernetes en el que podemos ejecutar contenedores?).
  4. Montaje paralelo, que se puede entender de diferentes maneras: diferentes comandos del Dockerfile (si se usa multietapa), varias confirmaciones del mismo repositorio, varios Dockerfiles.
  5. Montaje distribuido: Queremos recolectar cosas en grupos que sean "efímeras" porque su caché desaparece, lo que significa que debe almacenarse en algún lugar por separado.
  6. Finalmente, nombré el pináculo de los deseos. automágico: Lo ideal sería ir al repositorio, escribir algún comando y obtener una imagen lista para usar, ensamblada con la comprensión de cómo y qué hacer correctamente. Sin embargo, personalmente no estoy seguro de que se puedan prever todos los matices de esta manera.

Y aquí están los proyectos:

  • moby/kit de construcción — un constructor de Docker Inc (ya integrado en las versiones actuales de Docker), que intenta resolver todos estos problemas;
  • Kaniko — un constructor de Google que te permite construir sin Docker;
  • Buildpacks.io — El intento de CNCF de hacer magia automática y, en particular, una solución interesante con rebase para capas;
  • y un montón de otras utilidades, como construir, herramientas genuinas/img...

...y mira cuántas estrellas tienen en GitHub. Es decir, por un lado, docker build existe y puede hacer algo, pero en realidad el problema no está completamente resuelto - prueba de ello es el desarrollo paralelo de colectores alternativos, cada uno de los cuales resuelve parte de los problemas.

Asamblea en werf

Así que tenemos que patio (antes famoso como dapp) — Una utilidad de código abierto de la empresa Flant, que llevamos fabricando durante muchos años. Todo comenzó hace 5 años con scripts Bash que optimizaban el ensamblaje de Dockerfiles, y durante los últimos 3 años se ha llevado a cabo un desarrollo completo en el marco de un proyecto con su propio repositorio Git. (primero en Ruby, y luego reescrito to Go, y al mismo tiempo renombrado). ¿Qué problemas de montaje se resuelven en werf?

werf: nuestra herramienta para CI/CD en Kubernetes (resumen e informe en video)

Los problemas sombreados en azul ya se implementaron, la construcción paralela se realizó dentro del mismo host y se planea completar los problemas resaltados en amarillo para fines del verano.

Etapa de publicación en registro (publicar)

marcamos docker push... - ¿Qué podría resultar complicado al cargar una imagen en el registro? Y entonces surge la pregunta: “¿Qué etiqueta debo ponerle a la imagen?” Surge por la razón de que tenemos flujo de trabajo (u otra estrategia de Git) y Kubernetes, y la industria está tratando de garantizar que lo que sucede en Kubernetes siga lo que sucede en Git. Después de todo, Git es nuestra única fuente de verdad.

¿Qué tiene eso de difícil? Garantizar la reproducibilidad: de una confirmación en Git, que es de naturaleza inmutable (inmutable), a una imagen de Docker, que debe mantenerse igual.

También es importante para nosotros determinar el origen, porque queremos entender a partir de qué confirmación se creó la aplicación que se ejecuta en Kubernetes (luego podemos hacer diferencias y cosas similares).

Estrategias de etiquetado

El primero es sencillo. etiqueta git. Disponemos de un registro con una imagen etiquetada como 1.0. Kubernetes tiene escenario y producción, donde se sube esta imagen. En Git hacemos commits y en algún momento etiquetamos 2.0. Lo recopilamos según las instrucciones del repositorio y lo colocamos en el registro con la etiqueta 2.0. Lo llevamos al escenario y, si todo va bien, luego a producción.

werf: nuestra herramienta para CI/CD en Kubernetes (resumen e informe en video)

El problema con este enfoque es que primero colocamos la etiqueta y solo luego la probamos y la implementamos. ¿Por qué? En primer lugar, es simplemente ilógico: estamos lanzando una versión de software que ni siquiera hemos probado todavía (no podemos hacer otra cosa, porque para comprobarlo debemos poner una etiqueta). En segundo lugar, esta ruta no es compatible con Gitflow.

La segunda opción - git confirmar + etiqueta. La rama maestra tiene una etiqueta. 1.0; para ello en el registro: una imagen implementada en producción. Además, el clúster de Kubernetes tiene contornos de vista previa y preparación. A continuación seguimos Gitflow: en la rama principal de desarrollo (develop) creamos nuevas funciones, lo que resulta en una confirmación con el identificador #c1. Lo recopilamos y lo publicamos en el registro utilizando este identificador (#c1). Con el mismo identificador desplegamos la vista previa. Hacemos lo mismo con los compromisos. #c2 и #c3.

Cuando nos dimos cuenta de que hay suficientes funciones, comenzamos a estabilizar todo. Crea una rama en Git release_1.1 (En la base #c3 de develop). No es necesario recoger esta autorización porque... Esto se hizo en el paso anterior. Por lo tanto, podemos simplemente implementarlo en la etapa de preparación. Arreglamos errores en #c4 y de manera similar implementarlo en la puesta en escena. Al mismo tiempo, se está desarrollando develop, donde se toman cambios periódicamente de release_1.1. En algún momento, compilamos y cargamos un compromiso en la etapa de prueba, con lo cual estamos contentos (#c25).

Luego fusionamos (con avance rápido) la rama de lanzamiento (release_1.1) en maestro. Ponemos una etiqueta con la nueva versión en este compromiso (1.1). Pero esta imagen ya está recopilada en el registro, por lo que para no recopilarla nuevamente, simplemente agregamos una segunda etiqueta a la imagen existente (ahora tiene etiquetas en el registro). #c25 и 1.1). Después de eso, lo lanzamos a producción.

Existe el inconveniente de que solo se carga una imagen en la prueba (#c25), y en producción es algo diferente (1.1), pero sabemos que “físicamente” se trata de la misma imagen del registro.

werf: nuestra herramienta para CI/CD en Kubernetes (resumen e informe en video)

La verdadera desventaja es que no hay soporte para confirmaciones de fusión, hay que avanzar rápidamente.

Podemos ir más allá y hacer un truco... Veamos un ejemplo de un Dockerfile simple:

FROM ruby:2.3 as assets
RUN mkdir -p /app
WORKDIR /app
COPY . ./
RUN gem install bundler && bundle install
RUN bundle exec rake assets:precompile
CMD bundle exec puma -C config/puma.rb

FROM nginx:alpine
COPY --from=assets /app/public /usr/share/nginx/www/public

Construyamos un archivo a partir de él de acuerdo con el siguiente principio:

  • SHA256 de los identificadores de las imágenes utilizadas (ruby:2.3 и nginx:alpine), que son sumas de verificación de su contenido;
  • todos los equipos (RUN, CMD etcétera.);
  • SHA256 de los archivos que se agregaron.

... y tome la suma de comprobación (nuevamente SHA256) de dicho archivo. Este firma todo lo que define el contenido de la imagen de Docker.

werf: nuestra herramienta para CI/CD en Kubernetes (resumen e informe en video)

Volvamos al diagrama y en lugar de confirmaciones usaremos dichas firmas, es decir. etiquetar imágenes con firmas.

werf: nuestra herramienta para CI/CD en Kubernetes (resumen e informe en video)

Ahora, cuando sea necesario, por ejemplo, fusionar cambios de una versión a una maestra, podemos hacer una confirmación de fusión real: tendrá un identificador diferente, pero la misma firma. Con el mismo identificador lanzaremos la imagen a producción.

La desventaja es que ahora no será posible determinar qué tipo de compromiso se envió a producción: las sumas de verificación solo funcionan en una dirección. Este problema se resuelve con una capa adicional con metadatos; te contaré más más adelante.

Etiquetado en werf

En werf fuimos aún más lejos y nos estamos preparando para hacer una compilación distribuida con un caché que no está almacenado en una máquina... Entonces, estamos construyendo dos tipos de imágenes de Docker, las llamamos escenario и imagen.

El repositorio werf Git almacena instrucciones específicas de compilación que describen las diferentes etapas de la compilación (antes de instalar, instalar, antes de la configuración, Configure). Recopilamos la imagen de la primera etapa con una firma definida como suma de verificación de los primeros pasos. Luego agregamos el código fuente, para la nueva imagen del escenario calculamos su suma de verificación... Estas operaciones se repiten para todas las etapas, como resultado de lo cual obtenemos un conjunto de imágenes del escenario. Luego hacemos la imagen final, que también contiene metadatos sobre su origen. Y etiquetamos esta imagen de diferentes maneras (detalles más adelante).

werf: nuestra herramienta para CI/CD en Kubernetes (resumen e informe en video)

Supongamos que después de esto aparece una nueva confirmación en la que solo se ha cambiado el código de la aplicación. ¿Lo que sucederá? Para cambios de código, se creará un parche y se preparará una nueva imagen de escenario. Su firma se determinará como la suma de comprobación de la imagen del escenario anterior y el nuevo parche. Se formará una nueva imagen final a partir de esta imagen. Un comportamiento similar ocurrirá con cambios en otras etapas.

Por lo tanto, las imágenes de escenario son un caché que se puede almacenar de forma distribuida y las imágenes ya creadas a partir de él se cargan en Docker Registry.

werf: nuestra herramienta para CI/CD en Kubernetes (resumen e informe en video)

Limpiar el registro

No estamos hablando de eliminar capas que quedaron colgadas después de las etiquetas eliminadas; esta es una característica estándar del propio Docker Registry. Estamos hablando de una situación en la que se acumulan muchas etiquetas Docker y entendemos que algunas ya no las necesitamos, pero ocupan espacio (y/o pagamos por ello).

¿Cuáles son las estrategias de limpieza?

  1. simplemente no puedes hacer nada no limpiar. A veces es realmente más fácil pagar un poco por espacio extra que desenredar una enorme maraña de etiquetas. Pero esto sólo funciona hasta cierto punto.
  2. Reinicio completo. Si elimina todas las imágenes y reconstruye solo las actuales en el sistema CI, puede surgir un problema. Si el contenedor se reinicia en producción, se cargará una nueva imagen, una que aún no ha sido probada por nadie. Esto acaba con la idea de una infraestructura inmutable.
  3. Azul verde. Un registro comenzó a desbordarse: subimos imágenes a otro. El mismo problema que en el método anterior: ¿en qué momento se puede borrar el registro que ha empezado a desbordarse?
  4. Por tiempo. ¿Eliminar todas las imágenes de más de 1 mes? Pero definitivamente habrá un servicio que no se actualiza desde hace un mes...
  5. a mano determinar qué ya se puede eliminar.

Hay dos opciones realmente viables: no limpiar o una combinación azul-verde + manualmente. En el último caso, estamos hablando de lo siguiente: cuando comprende que es hora de limpiar el registro, crea uno nuevo y le agrega todas las imágenes nuevas en el transcurso de, por ejemplo, un mes. Y después de un mes, vea qué pods en Kubernetes todavía usan el registro anterior y transfiéralos también al nuevo registro.

A qué hemos llegado patio? Nosotros coleccionamos:

  1. Git head: todas las etiquetas, todas las ramas, asumiendo que necesitamos todo lo que está etiquetado en Git en las imágenes (y si no, entonces debemos eliminarlo en el propio Git);
  2. todos los pods que actualmente están enviados a Kubernetes;
  3. ReplicaSets antiguos (lo que se lanzó recientemente), y también planeamos escanear las versiones de Helm y seleccionar las imágenes más recientes allí.

... y haga una lista blanca a partir de este conjunto: una lista de imágenes que no eliminaremos. Limpiamos todo lo demás, después de lo cual encontramos imágenes de etapa huérfanas y las eliminamos también.

Etapa de implementación

Declaratividad confiable

El primer punto sobre el que me gustaría llamar la atención en la implementación es el lanzamiento de la configuración de recursos actualizada, declarada de forma declarativa. El documento YAML original que describe los recursos de Kubernetes siempre es muy diferente del resultado que realmente se ejecuta en el clúster. Porque Kubernetes agrega a la configuración:

  1. identificadores;
  2. servicio de información;
  3. muchos valores predeterminados;
  4. sección con estado actual;
  5. cambios realizados como parte del webhook de admisión;
  6. el resultado del trabajo de varios controladores (y el planificador).

Por lo tanto, cuando aparece una nueva configuración de recursos (nueva), no podemos simplemente tomar y sobrescribir la configuración actual "en vivo" con ella (para vivir). Para ello tendremos que comparar nueva con la última configuración aplicada (último aplicado) y rodar hacia para vivir parche recibido.

Este enfoque se llama Fusión de 2 vías. Se utiliza, por ejemplo, en Helm.

También hay Fusión de 3 vías, que se diferencia en que:

  • comparando último aplicado и nueva, miramos lo que se eliminó;
  • comparando nueva и para vivir, miramos lo que se ha agregado o cambiado;
  • el parche sumado se aplica a para vivir.

Implementamos más de 1000 aplicaciones con Helm, por lo que en realidad vivimos con una combinación bidireccional. Sin embargo, tiene una serie de problemas que hemos solucionado con nuestros parches, que ayudan a que Helm funcione con normalidad.

Estado de implementación real

Después de que nuestro sistema de CI genera una nueva configuración para Kubernetes basada en el siguiente evento, la transmite para su uso. (aplicar) a un clúster - usando Helm o kubectl apply. A continuación, se produce la fusión de N vías ya descrita, a la que la API de Kubernetes responde con aprobación al sistema CI y a su usuario.

werf: nuestra herramienta para CI/CD en Kubernetes (resumen e informe en video)

Sin embargo, existe un gran problema: después de todo Una aplicación exitosa no significa una implementación exitosa. Si Kubernetes entiende qué cambios se deben aplicar y los aplica, todavía no sabemos cuál será el resultado. Por ejemplo, actualizar y reiniciar pods en el frontend puede tener éxito, pero no en el backend, y obtendremos diferentes versiones de las imágenes de la aplicación en ejecución.

Para hacer todo correctamente, este esquema requiere un enlace adicional: un rastreador especial que recibirá información de estado de la API de Kubernetes y la transmitirá para un análisis más detallado del estado real de las cosas. Creamos una biblioteca de código abierto en Go - perro cubo (ver su anuncio aquí), que resuelve este problema y está integrado en werf.

El comportamiento de este rastreador a nivel werf se configura mediante anotaciones que se colocan en Deployments o StatefulSets. Anotación principal - fail-mode - entiende los siguientes significados:

  • IgnoreAndContinueDeployProcess — ignoramos los problemas que plantea el despliegue de este componente y continuamos con el despliegue;
  • FailWholeDeployProcessImmediately — un error en este componente detiene el proceso de implementación;
  • HopeUntilEndOfDeployProcess — Esperamos que este componente funcione al final de la implementación.

Por ejemplo, esta combinación de recursos y valores de anotación fail-mode:

werf: nuestra herramienta para CI/CD en Kubernetes (resumen e informe en video)

Cuando implementamos por primera vez, es posible que la base de datos (MongoDB) aún no esté lista; las implementaciones fallarán. Pero puedes esperar el momento en que comience y el despliegue aún se llevará a cabo.

Hay dos anotaciones más para kubedog en werf:

  • failures-allowed-per-replica — el número de caídas permitidas para cada réplica;
  • show-logs-until — regula el momento hasta el cual werf muestra (en stdout) los registros de todos los pods implementados. El valor predeterminado es PodIsReady (para ignorar mensajes que probablemente no queramos cuando el tráfico comience a llegar al pod), pero los valores también son válidos: ControllerIsReady и EndOfDeploy.

¿Qué más queremos del despliegue?

Además de los dos puntos ya descritos, nos gustaría:

  • ver registros - y sólo los necesarios, y no todos seguidos;
  • pista прогресс, porque si el trabajo se cuelga “en silencio” durante varios minutos, es importante comprender qué está sucediendo allí;
  • иметь reversión automática en caso de que algo haya salido mal (y por tanto es fundamental conocer el estado real del despliegue). El despliegue debe ser atómico: o llega hasta el final o todo vuelve a su estado anterior.

resultados

Para nosotros, como empresa, para implementar todos los matices descritos en las diferentes etapas de entrega (construcción, publicación, implementación), un sistema de CI y una utilidad son suficientes. patio.

En lugar de una conclusión:

werf: nuestra herramienta para CI/CD en Kubernetes (resumen e informe en video)

Con la ayuda de werf, hemos logrado grandes avances en la solución de una gran cantidad de problemas para los ingenieros de DevOps y nos alegraría que la comunidad en general al menos probara esta utilidad en acción. Juntos será más fácil lograr un buen resultado.

Vídeos y diapositivas

Vídeo de la actuación (~47 minutos):

Presentación del informe:

PS

Otros informes sobre Kubernetes en nuestro blog:

Fuente: habr.com

Añadir un comentario