Propongo leer la transcripción del informe de Alexander Sigachev de Inventos "Proceso de desarrollo y pruebas con Docker + Gitlab CI"
Aquellos que recién comienzan a implementar el proceso de desarrollo y prueba basado en Docker + Gitlab CI a menudo hacen preguntas básicas. ¿Dónde empezar? ¿Cómo organizar? ¿Cómo probar?
Este informe es bueno porque habla de manera estructurada sobre el proceso de desarrollo y prueba usando Docker y Gitlab CI. El informe en sí es de 2017. Creo que de este informe puedes aprender los conceptos básicos, la metodología, la idea, la experiencia de uso.

A quién le importa, por favor debajo del gato.
Mi nombre es Alexander Sigachev. Trabajo para Inventos. Les cuento mi experiencia con el uso de Docker y como poco a poco lo vamos implementando en los proyectos de la empresa.
Tema de presentación: Proceso de desarrollo usando Docker y Gitlab CI.

Esta es mi segunda charla sobre Docker. En el momento del primer informe, solo usábamos Docker en Desarrollo en máquinas de desarrollo. La cantidad de empleados que usaron Docker fue de aproximadamente 2-3 personas. Poco a poco, se ganó experiencia y avanzamos un poco más. Enlace a nuestro .
¿Qué habrá en este informe? Compartiremos nuestra experiencia sobre qué rake hemos recaudado, qué problemas hemos resuelto. No en todas partes era hermoso, pero permitía seguir adelante.
Nuestro lema es: atracar todo lo que podamos.

¿Qué problemas estamos resolviendo?
Cuando hay varios equipos en una empresa, el programador es un recurso compartido. Hay etapas en las que se saca a un programador de un proyecto y se le entrega durante algún tiempo a otro proyecto.
Para que el programador entienda rápidamente, necesita descargar el código fuente del proyecto e iniciar el entorno lo antes posible, lo que le permitirá avanzar más resolviendo los problemas de este proyecto.
Por lo general, si comienza desde cero, hay poca documentación en el proyecto. La información sobre cómo configurar está disponible solo para los veteranos. Los empleados instalan su lugar de trabajo por su cuenta en uno o dos días. Para acelerar esto, usamos Docker.
La siguiente razón es la estandarización de la configuración en Desarrollo. En mi experiencia, los desarrolladores siempre toman la iniciativa. En cada quinto caso, se ingresa un dominio personalizado, por ejemplo, vasya.dev. Sentado junto a él está su vecino Petya, cuyo dominio es petya.dev. Desarrollan un sitio web o algún componente del sistema utilizando este nombre de dominio.
Cuando el sistema crece y estos nombres de dominio comienzan a entrar en configuraciones, surge un conflicto en el entorno de desarrollo y se reescribe la ruta del sitio.
Lo mismo sucede con la configuración de la base de datos. Alguien no se preocupa por la seguridad y trabaja con una contraseña de root vacía. En la etapa de instalación, MySQL le pidió a alguien una contraseña y la contraseña resultó ser 123. A menudo sucede que la configuración de la base de datos cambia constantemente según la confirmación del desarrollador. Alguien corrigió, alguien no corrigió la configuración. Hubo trucos cuando sacamos algún tipo de configuración de prueba en .gitignore y cada desarrollador tuvo que instalar la base de datos. Esto dificultó el comienzo. Es necesario, entre otras cosas, recordar acerca de la base de datos. Se debe inicializar la base de datos, se debe ingresar una contraseña, se debe registrar un usuario, se debe crear una tabla, etc.
Otro problema son las diferentes versiones de las bibliotecas. A menudo sucede que un desarrollador trabaja con diferentes proyectos. Hay un proyecto Legacy que comenzó hace cinco años (desde 2017 - nota del editor). En el momento del lanzamiento, comenzamos con MySQL 5.5. También hay proyectos modernos en los que tratamos de implementar versiones más modernas de MySQL, por ejemplo, 5.7 o anteriores (en 2017 - nota editorial)
Cualquiera que trabaje con MySQL sabe que estas bibliotecas traen consigo dependencias. Es bastante problemático ejecutar 2 bases juntas. Al menos, los clientes antiguos son problemáticos para conectarse a la nueva base de datos. Esto a su vez crea varios problemas.
El siguiente problema es cuando un desarrollador trabaja en una máquina local, usa recursos locales, archivos locales, RAM local. Toda interacción a la hora de desarrollar una solución a los problemas se lleva a cabo en el marco del hecho de que trabaja en una sola máquina. Un ejemplo es cuando tenemos servidores back-end en Production 3, y el desarrollador guarda archivos en el directorio raíz y desde allí nginx toma archivos para responder a la solicitud. Cuando dicho código entra en Producción, resulta que el archivo está presente en uno de los 3 servidores.
La dirección de los microservicios se está desarrollando ahora. Cuando dividimos nuestras grandes aplicaciones en algunos pequeños componentes que interactúan entre sí. Esto le permite seleccionar tecnologías para una pila específica de tareas. También le permite compartir el trabajo y las responsabilidades entre los desarrolladores.
El desarrollador de Frondend, que desarrolla en JS, casi no tiene influencia en Backend. El desarrollador backend, a su vez, desarrolla, en nuestro caso, Ruby on Rails y no interfiere con Frondend. La interacción se realiza mediante la API.
Como beneficio adicional, con la ayuda de Docker, pudimos reciclar recursos en Staging. Cada proyecto, debido a sus especificidades, requería ciertos ajustes. Físicamente, era necesario asignar un servidor virtual y configurarlos por separado, o compartir algún tipo de entorno variable y los proyectos podrían, según la versión de las bibliotecas, influirse entre sí.

Herramientas. ¿Qué usamos?
- Docker mismo. El Dockerfile describe las dependencias de una sola aplicación.
- Docker-compose es un paquete que reúne algunas de nuestras aplicaciones Docker.
- Usamos GitLab para almacenar el código fuente.
- Usamos GitLab-CI para la integración del sistema.

El informe consta de dos partes.
La primera parte hablará sobre cómo se ejecutó Docker en las máquinas de los desarrolladores.
La segunda parte hablará sobre cómo interactuar con GitLab, cómo ejecutamos pruebas y cómo implementamos Staging.

Docker es una tecnología que permite (mediante un enfoque declarativo) describir los componentes necesarios. Este es un archivo Docker de ejemplo. Aquí declaramos que heredamos de la imagen Docker oficial de Ruby:2.3.0. Contiene Ruby versión 2.3 instalada. Instalamos las bibliotecas de compilación requeridas y NodeJS. Describimos que creamos un directorio /app. Establezca el directorio de la aplicación como el directorio de trabajo. En este directorio colocamos el Gemfile y el Gemfile.lock mínimos requeridos. Luego construimos los proyectos que instalan esta imagen de dependencia. Indicamos que el contenedor estará listo para escuchar en el puerto externo 3000. El último comando es el comando que lanza directamente nuestra aplicación. Si ejecutamos el comando de inicio del proyecto, la aplicación intentará ejecutar y ejecutar el comando especificado.

Este es un ejemplo mínimo de un archivo docker-compose. En este caso, mostramos que hay una conexión entre dos contenedores. Esto es directamente en el servicio de base de datos y el servicio web. Nuestras aplicaciones web en la mayoría de los casos requieren algún tipo de base de datos como backend para almacenar datos. Como estamos usando MySQL, el ejemplo es con MySQL, pero nada nos impide usar alguna otra base de datos (PostgreSQL, Redis).
Tomamos de la fuente oficial del concentrador Docker la imagen de MySQL 5.7.14 sin cambios. Recopilamos la imagen responsable de nuestra aplicación web del directorio actual. Recoge una imagen para nosotros durante el primer lanzamiento. Luego ejecuta el comando que estamos ejecutando aquí. Si retrocedemos, veremos que se ha definido el comando de lanzamiento a través de Puma. Puma es un servicio escrito en Ruby. En el segundo caso, anulamos. Este comando puede ser arbitrario dependiendo de nuestras necesidades o tareas.
También describimos que necesitamos reenviar un puerto en nuestra máquina host de desarrollador de 3000 a 3000 en el puerto del contenedor. Esto se hace automáticamente usando iptables y su mecanismo, que está integrado directamente en Docker.
El desarrollador también puede, como antes, acceder a cualquier dirección IP disponible, por ejemplo, 127.0.0.1 es la dirección IP local o externa de la máquina.
La última línea dice que el contenedor web depende del contenedor db. Cuando llamamos al inicio del contenedor web, docker-compose primero iniciará la base de datos por nosotros. Ya al inicio de la base de datos (de hecho, ¡después del lanzamiento del contenedor! Esto no garantiza que la base de datos esté lista) lanzará la aplicación, nuestro backend.
Esto evita errores cuando la base de datos no se abre y ahorra recursos cuando detenemos el contenedor de la base de datos, liberando así recursos para otros proyectos.

Lo que nos da el uso de la dockerización de base de datos en el proyecto. Arreglamos la versión de MySQL para todos los desarrolladores. Esto evita algunos errores que pueden ocurrir cuando las versiones divergen, cuando cambia la sintaxis, la configuración y las configuraciones predeterminadas. Esto le permite especificar un nombre de host común para la base de datos, inicio de sesión, contraseña. Nos estamos alejando del zoológico de nombres y conflictos en los archivos de configuración que teníamos antes.
Tenemos la oportunidad de utilizar una configuración más óptima para el entorno de desarrollo, que diferirá de la predeterminada. MySQL está configurado para máquinas débiles de forma predeterminada y su rendimiento de fábrica es muy pobre.

Docker le permite usar el intérprete Python, Ruby, NodeJS, PHP de la versión deseada. Nos deshacemos de la necesidad de usar algún tipo de administrador de versiones. Anteriormente, Ruby usaba un paquete rpm que le permitía cambiar la versión según el proyecto. También permite, gracias al contenedor Docker, migrar sin problemas el código y versionarlo junto con las dependencias. No tenemos problema en entender la versión tanto del intérprete como del código. Para actualizar la versión, baje el contenedor antiguo y suba el contenedor nuevo. Si algo salió mal, podemos bajar el contenedor nuevo y subir el contenedor viejo.
Después de construir la imagen, los contenedores tanto en Desarrollo como en Producción serán los mismos. Esto es especialmente cierto para las grandes instalaciones.
En Frontend usamos JavaScipt y NodeJS.
Ahora tenemos el último proyecto en ReacJS. El desarrollador ejecutó todo en el contenedor y desarrolló usando hot-reload.
A continuación, se inicia la tarea de ensamblaje de JavaScipt y el código compilado en estática se proporciona a través de recursos de ahorro de nginx.

Aquí he dado el esquema de nuestro último proyecto.
¿Qué tareas se resolvieron? Teníamos la necesidad de construir un sistema con el que interactúen los dispositivos móviles. Reciben datos. Una posibilidad es enviar notificaciones automáticas a este dispositivo.
¿Qué hemos hecho para esto?
Dividimos la aplicación en componentes tales como: la parte de administración en JS, el backend, que funciona a través de la interfaz REST en Ruby on Rails. El backend interactúa con la base de datos. El resultado que se genera se entrega al cliente. El panel de administración interactúa con el backend y la base de datos a través de la interfaz REST.
También teníamos la necesidad de enviar notificaciones automáticas. Antes de eso, teníamos un proyecto que implementaba un mecanismo que se encarga de enviar notificaciones a las plataformas móviles.
Hemos desarrollado el siguiente esquema: un operador del navegador interactúa con el panel de administración, el panel de administración interactúa con el backend, la tarea es enviar notificaciones Push.
Las notificaciones push interactúan con otro componente que se implementa en NodeJS.
Se construyen colas y luego se envían notificaciones según su mecanismo.
Aquí se dibujan dos bases de datos. Por el momento, con la ayuda de Docker, utilizamos 2 bases de datos independientes que no están relacionadas entre sí de ninguna manera. Además, tienen una red virtual común y los datos físicos se almacenan en diferentes directorios en la máquina del desarrollador.

Lo mismo pero en números. Aquí es donde la reutilización del código es importante.
Si antes hablábamos de reutilizar código en forma de bibliotecas, en este ejemplo, nuestro servicio que responde a las notificaciones Push se reutiliza como un servidor completo. Proporciona una API. Y nuestro nuevo desarrollo ya interactúa con él.
En ese momento, estábamos usando la versión 4 de NodeJS. Ahora (en 2017 - nota del editor) en desarrollos recientes usamos la versión 7 de NodeJS. No hay problema en que nuevos componentes impliquen nuevas versiones de bibliotecas.
Si es necesario, puede refactorizar y elevar la versión de NodeJS desde el servicio de notificaciones Push.
Y si podemos mantener la compatibilidad con la API, será posible reemplazarla con otros proyectos que se usaron anteriormente.

¿Qué necesitas para agregar Docker? Agregamos un Dockerfile a nuestro repositorio, que describe las dependencias necesarias. En este ejemplo, los componentes se desglosan lógicamente. Este es el conjunto mínimo de un desarrollador backend.
Al crear un nuevo proyecto, creamos un Dockerfile, describimos el ecosistema deseado (Python, Ruby, NodeJS). En docker-compose, describe la dependencia necesaria: la base de datos. Describimos que necesitamos una base de datos de tal y tal versión, almacenar datos allí y allá.
Usamos un tercer contenedor separado con nginx para servir estática. Es posible subir fotos. Backend los coloca en un volumen previamente preparado, que también se monta en un contenedor con nginx, lo que proporciona la estática.
Para almacenar la configuración de nginx, mysql, agregamos una carpeta Docker en la que almacenamos las configuraciones necesarias. Cuando un desarrollador hace un clon de git de un repositorio en su máquina, ya tiene un proyecto listo para el desarrollo local. No hay duda de qué puerto o qué configuración aplicar.

A continuación, tenemos varios componentes: admin, inform-API, notificaciones push.
Para poner en marcha todo esto, creamos otro repositorio, al que llamamos dockerized-app. Actualmente usamos varios repositorios antes de cada componente. Simplemente son lógicamente diferentes: en GitLab parece una carpeta, pero en la máquina del desarrollador, una carpeta para un proyecto específico. Un nivel más abajo están los componentes que se combinarán.

Este es un ejemplo de solo el contenido de dockerized-app. También traemos aquí el directorio Docker, en el que completamos las configuraciones requeridas para las interacciones de todos los componentes. Hay un README.md que describe brevemente cómo ejecutar el proyecto.
Aquí hemos aplicado dos archivos docker-compose. Esto se hace para poder ejecutarse en pasos. Cuando un desarrollador trabaja con el núcleo, no necesita notificaciones automáticas, simplemente inicia un archivo docker-compose y, en consecuencia, el recurso se guarda.
Si es necesario integrarse con notificaciones push, se lanzan docker-compose.yaml y docker-compose-push.yaml.
Dado que docker-compose.yaml y docker-compose-push.yaml están en una carpeta, se crea automáticamente una sola red virtual.

Descripción de los componentes. Este es un archivo más avanzado que es responsable de la recopilación de componentes. ¿Qué es notable aquí? Aquí presentamos el componente equilibrador.
Esta es una imagen de Docker lista para usar que ejecuta nginx y una aplicación que escucha en el socket de Docker. Dinámico, a medida que los contenedores se activan y desactivan, regenera la configuración de nginx. Distribuimos el manejo de componentes por nombres de dominio de tercer nivel.
Para el entorno de desarrollo, usamos el dominio .dev - api.informer.dev. Las aplicaciones con un dominio .dev están disponibles en la máquina local del desarrollador.
Además, las configuraciones se transfieren a cada proyecto y todos los proyectos se inician juntos al mismo tiempo.

Gráficamente resulta que el cliente es nuestro navegador o alguna herramienta con la que hacemos peticiones al balanceador.
El equilibrador de nombres de dominio determina con qué contenedor ponerse en contacto.
Puede ser nginx, que le da al administrador JS. Esto puede ser nginx, que brinda la API, o archivos estáticos, que se entregan a nginx en forma de carga de imágenes.
El diagrama muestra que los contenedores están conectados por una red virtual y ocultos detrás de un proxy.
En la máquina del desarrollador, puede acceder al contenedor sabiendo la IP, pero en principio no usamos esto. Prácticamente no hay necesidad de acceso directo.

¿Qué ejemplo mirar para dockerizar su aplicación? En mi opinión, un buen ejemplo es la imagen oficial de Docker para MySQL.
Es bastante desafiante. Hay muchas versiones. Pero su funcionalidad le permite cubrir muchas necesidades que puedan surgir en el proceso de desarrollo posterior. Si dedica tiempo y descubre cómo interactúa todo, entonces creo que no tendrá problemas en la autoimplementación.
Hub.docker.com generalmente contiene enlaces a github.com, que contiene datos sin procesar directamente a partir de los cuales puede crear la imagen usted mismo.
Además, en este repositorio hay un script docker-endpoint.sh, que es responsable de la inicialización inicial y del procesamiento posterior del lanzamiento de la aplicación.
También en este ejemplo, existe la posibilidad de configurar mediante variables de entorno. Al definir una variable de entorno cuando se ejecuta un solo contenedor o a través de docker-compose, podemos decir que necesitamos establecer una contraseña vacía para que docker haga root en MySQL o lo que queramos.
Hay una opción para crear una contraseña aleatoria. Decimos que necesitamos un usuario, necesitamos establecer una contraseña para el usuario y necesitamos crear una base de datos.
En nuestros proyectos, unificamos ligeramente el Dockerfile, que es responsable de la inicialización. Allí lo corregimos a nuestras necesidades para que sea solo una extensión de los derechos de usuario que usa la aplicación. Esto nos permitió simplemente crear una base de datos desde la consola de la aplicación más adelante. Las aplicaciones de Ruby tienen un comando para crear, modificar y eliminar bases de datos.

Este es un ejemplo de cómo se ve una versión específica de MySQL en github.com. Puede abrir el Dockerfile y ver cómo va la instalación allí.
docker-endpoint.sh es el script responsable del punto de entrada. Durante la inicialización inicial, se requieren algunos pasos de preparación y todas estas acciones se llevan a cabo solo en el script de inicialización.

Pasamos a la segunda parte.
Para almacenar los códigos fuente, cambiamos a gitlab. Este es un sistema bastante poderoso que tiene una interfaz visual.
Uno de los componentes de Gitlab es Gitlab CI. Le permite describir una secuencia de comandos que luego se usará para organizar un sistema de entrega de código o ejecutar pruebas automáticas.
Charla de Gitlab CI 2 - informe del club Ruby Russia - bastante detallado y tal vez te interese.

Ahora veremos qué se requiere para activar Gitlab CI. Para iniciar Gitlab CI, solo necesitamos colocar el archivo .gitlab-ci.yml en la raíz del proyecto.
Aquí describimos que queremos ejecutar una secuencia de estados como una prueba, implementación.
Ejecutamos scripts que llaman directamente a docker-compose para construir nuestra aplicación. Este es solo un ejemplo de back-end.
A continuación, decimos que es necesario ejecutar migraciones para cambiar la base de datos y ejecutar pruebas.
Si los scripts se ejecutan correctamente y no devuelven un código de error, el sistema continúa con la segunda etapa de la implementación.
La etapa de implementación se implementa actualmente para la preparación. No organizamos un reinicio sin tiempo de inactividad.
Apagamos a la fuerza todos los contenedores y luego volvemos a levantar todos los contenedores, recogidos en la primera etapa durante la prueba.
Estamos ejecutando para la variable de entorno actual las migraciones de base de datos que escribieron los desarrolladores.
Hay una nota de que esto se aplica solo a la rama principal.
Al cambiar otras ramas no se ejecuta.
Es posible organizar los despliegues por sucursales.

Para organizar esto aún más, necesitamos instalar Gitlab Runner.
Esta utilidad está escrita en Golang. Es un archivo único, como es común en el mundo de Golang, que no requiere ninguna dependencia.
Al inicio, registramos el Gitlab Runner.
Obtenemos la clave en la interfaz web de Gitlab.
Luego llamamos al comando de inicialización en la línea de comando.
Configurar Gitlab Runner de forma interactiva (Shell, Docker, VirtualBox, SSH)
El código en Gitlab Runner se ejecutará en cada confirmación, según la configuración de .gitlab-ci.yml.

Cómo se ve visualmente en Gitlab en la interfaz web. Después de haber conectado GItlab CI, tenemos un indicador que muestra el estado de la compilación en este momento.
Vemos que se realizó un commit hace 4 minutos, el cual pasó todas las pruebas y no causó ningún problema.

Podemos echar un vistazo más de cerca a las construcciones. Aquí vemos que ya han pasado dos estados. Estado de prueba y estado de implementación en preparación.
Si hacemos clic en una compilación específica, habrá una salida de consola de los comandos que se ejecutaron en el proceso de acuerdo con .gitlab-ci.yml.

Así es como se ve nuestro historial de productos. Vemos que hubo intentos exitosos. Cuando se envían las pruebas, no se continúa con el siguiente paso y el código de preparación no se actualiza.

¿Qué tareas resolvimos en la puesta en escena cuando implementamos docker? Nuestro sistema consta de componentes y tuvimos la necesidad de reiniciar, solo una parte de los componentes que se actualizaron en el repositorio, y no todo el sistema.
Para hacer esto, tuvimos que aplastar todo en carpetas separadas.
Después de hacer esto, tuvimos un problema con el hecho de que Docker-compose crea su propio espacio de red para cada papá y no ve los componentes del vecino.
Para movernos, creamos la red en Docker manualmente. Se escribió en Docker-compose que usa una red de este tipo para este proyecto.
Por lo tanto, cada componente que comienza con esta malla ve componentes en otras partes del sistema.
El siguiente problema es dividir la puesta en escena en varios proyectos.
Ya que para que todo esto se vea hermoso y lo más cercano posible a la producción, es bueno usar el puerto 80 o 443, que se usa en todas partes en la WEB.

¿Cómo lo solucionamos? Hemos asignado un Gitlab Runner a todos los proyectos principales.
Gitlab le permite ejecutar varios Gitlab Runners distribuidos, que simplemente tomarán todas las tareas por turnos de manera caótica y las ejecutarán.
Para que no tengamos una casa, limitamos el grupo de nuestros proyectos a un Gitlab Runner, que se adapta sin problemas a nuestros volúmenes.
Movimos nginx-proxy a un script de inicio separado y agregamos cuadrículas para todos los proyectos en él.
Nuestro proyecto tiene una cuadrícula y el equilibrador tiene varias cuadrículas por nombre de proyecto. Puede representar más a través de nombres de dominio.
Nuestras solicitudes llegan a través del dominio en el puerto 80 y se resuelven en un grupo de contenedores que sirve a este dominio.

¿Qué otros problemas había? Esto es lo que todos los contenedores ejecutan como raíz de forma predeterminada. Esta es una raíz diferente al host raíz del sistema.
Sin embargo, si ingresa al contenedor, será root y el archivo que creamos en este contenedor obtiene derechos de root.
Si el desarrollador ingresó al contenedor e hizo algunos comandos allí que generan archivos, luego abandonó el contenedor, entonces tiene un archivo en su directorio de trabajo al que no tiene acceso.
¿Cómo se puede solucionar? Puede agregar usuarios que estarán en el contenedor.
¿Qué problemas surgieron cuando añadimos al usuario?
Al crear un usuario, a menudo no tenemos el mismo ID de grupo (UID) e ID de usuario (GID).
Para resolver este problema en el contenedor, usamos usuarios con ID 1000.
En nuestro caso, esto coincidió con el hecho de que casi todos los desarrolladores usan el sistema operativo. UbuntuY el sistema operativo Ubuntu El primer usuario tiene el ID 1000.

¿Tenemos planes?
Lea la documentación de Docker. El proyecto se está desarrollando activamente, la documentación está cambiando. Los datos que se recibieron hace dos o tres meses ya se están quedando obsoletos lentamente.
Es muy posible que algunos de los problemas que resolvimos ya estén resueltos por medios estándar.
Así que quiero ir más allá para ir directamente a la orquestación.
Un ejemplo es el mecanismo integrado de Docker llamado Docker Swarm, que viene listo para usar. Quiero ejecutar algo en producción basado en la tecnología Docker Swarm.
Los contenedores de desove hacen que sea un inconveniente trabajar con registros. Ahora los registros están aislados. Están dispersos en contenedores. Una de las tareas es facilitar el acceso a los registros a través de la interfaz web.

Fuente: habr.com
