El sistema de recomendación de contenido de video en línea en el que estamos trabajando es un desarrollo comercial cerrado y técnicamente es un clúster de componentes múltiples con componentes propios y de código abierto. El propósito de este artículo es describir la introducción de un sistema de agrupación de enjambres de Docker para una plataforma de ensayo, sin interrumpir el flujo de trabajo existente de nuestros procesos en un tiempo limitado. La narrativa presentada a su atención se divide en dos partes. La primera parte describe el CI / CD antes de usar Docker Swarm, y la segunda describe el proceso de implementación. Aquellos que no estén interesados en leer la primera parte pueden pasar de manera segura a la segunda.
Parte 1
En el año distante, distante, se requería configurar el proceso de CI / CD lo más rápido posible. Una de las condiciones era no usar Docker
para implementar los componentes desarrollados por varias razones:
- para una operación más confiable y estable de los componentes en Producción (es decir, el requisito de no usar la virtualización)
- Los principales desarrolladores no querían trabajar con Docker (extraño, pero era solo eso)
- por razones ideológicas gestión de I + D
La infraestructura, la pila y los requisitos iniciales de muestra para MVP fueron los siguientes:
- 4 servidores Intel® X5650 con Debian (una máquina más poderosa completamente para desarrollo)
- El desarrollo de componentes personalizados se lleva a cabo en C ++, Python3
- Las principales herramientas utilizadas por terceros: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, ...
- Montaje de tuberías y prueba de componentes por separado para depuración y liberación
Uno de los primeros problemas a resolver en la etapa inicial es cómo implementar componentes personalizados en cualquier entorno (CI / CD).
Los componentes de terceros decidieron instalar sistémicamente y actualizarlos sistémicamente. Las aplicaciones personalizadas desarrolladas en C ++ o Python se pueden implementar de varias maneras. Entre ellos, por ejemplo: crear paquetes de sistema, enviarlos al repositorio de imágenes recopiladas y su posterior instalación en servidores. Por una razón desconocida, se eligió otro método, a saber, usar CI, se compilan los archivos de aplicación ejecutables, se crea un entorno de proyecto virtual, se instalan módulos py de require.txt y todos estos artefactos se envían junto con las configuraciones, los scripts y el entorno de aplicación que acompaña a los servidores. A continuación, las aplicaciones se inician desde un usuario virtual sin derechos de administrador.
Gitlab-CI fue elegido como el sistema CI / CD. La tubería resultante se parecía a esto:

Estructuralmente, gitlab-ci.yml se veía así--- variables: # , CMAKE_CPUTYPE: "westmere" DEBIAN: "MYREGISTRY:5000/debian:latest" before_script: - eval $(ssh-agent -s) - ssh-add <(echo "$SSH_PRIVATE_KEY") - mkdir -p ~/.ssh && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config stages: - build - testing - deploy debug.debian: stage: build image: $DEBIAN script: - cd builds/release && ./build.sh paths: - bin/ - builds/release/bin/ when: always release.debian: stage: build image: $DEBIAN script: - cd builds/release && ./build.sh paths: - bin/ - builds/release/bin/ when: always ## testing stage tests.codestyle: stage: testing image: $DEBIAN dependencies: - release.debian script: - /bin/bash run_tests.sh -t codestyle -b "${CI_COMMIT_REF_NAME}_codestyle" tests.debug.debian: stage: testing image: $DEBIAN dependencies: - debug.debian script: - /bin/bash run_tests.sh -e codestyle/test_pylint.py -b "${CI_COMMIT_REF_NAME}_debian_debug" artifacts: paths: - run_tests/username/ when: always expire_in: 1 week tests.release.debian: stage: testing image: $DEBIAN dependencies: - release.debian script: - /bin/bash run_tests.sh -e codestyle/test_pylint.py -b "${CI_COMMIT_REF_NAME}_debian_release" artifacts: paths: - run_tests/username/ when: always expire_in: 1 week ## staging stage deploy_staging: stage: deploy environment: staging image: $DEBIAN dependencies: - release.debian script: - cd scripts/deploy/ && python3 createconfig.py -s $CI_ENVIRONMENT_NAME && /bin/bash install_venv.sh -d -r ../../requirements.txt && python3 prepare_init.d.py && python3 deploy.py -s $CI_ENVIRONMENT_NAME when: manual
Vale la pena señalar que el montaje y las pruebas se realizan en su propia imagen, donde todos los paquetes necesarios del sistema ya están instalados y se realizan otras configuraciones.
Aunque cada uno de estos scripts en el trabajo es interesante a su manera,
ciertamente no hablaré sobre ellos , pero la descripción de cada uno de ellos tomará un tiempo considerable y este no es el propósito del artículo. Solo prestaré atención al hecho de que la etapa de implementación consiste en una secuencia de llamadas de script:
- createconfig.py : crea el archivo settings.ini con la configuración de los componentes en un entorno diferente para la implementación posterior (Preproducción, Producción, Pruebas, ...)
- install_venv.sh : crea un entorno virtual para componentes py en un directorio específico y lo copia en servidores remotos
- prepare_init.d.py : prepara scripts de inicio y detención de componentes basados en una plantilla
- deploy.py : descomprime y reinicia nuevos componentes
El tiempo paso La etapa de puesta en escena ha sido reemplazada por preproducción y producción. El soporte del producto se agregó en otro kit de distribución (CentOS). Se agregaron 5 servidores físicos más potentes y una docena de servidores virtuales. Y se hizo cada vez más difícil para los desarrolladores y evaluadores ejecutar sus tareas en un entorno más o menos cercano al estado de trabajo. En este momento, quedó claro que es imposible prescindir de él ...
Parte II

Por lo tanto, nuestro clúster sigue
siendo un espectáculo de un sistema de un par de docenas de componentes separados que no están descritos por Dockerfiles. Puede configurarlo para la implementación en un entorno específico solo como un todo. Nuestra tarea es implementar el clúster en un entorno provisional para ejecutarlo antes de las pruebas previas al lanzamiento.
Teóricamente, puede haber varios grupos de trabajo simultáneos: tantas como tareas están en el estado completado o cerca de completarse. Las capacidades disponibles para nuestros servidores nos permiten ejecutar varios clústeres en cada servidor. Cada grupo de etapas debe estar aislado (no debe haber intersección en puertos, directorios, etc.).
El recurso más valioso es nuestro tiempo, y no teníamos mucho.
Para un comienzo más rápido, eligieron Docker Swarm debido a su simplicidad y flexibilidad de arquitectura. Lo primero que hicimos fue crear en los servidores del administrador remoto y varios nodos:
$ docker node ls ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION kilqc94pi2upzvabttikrfr5d nop-test-1 Ready Active 19.03.2 jilwe56pl2zvabupryuosdj78 nop-test-2 Ready Active 19.03.2 j5a4yz1kr2xke6b1ohoqlnbq5 * nop-test-3 Ready Active Leader 19.03.2
A continuación, creamos una red:
$ docker network create --driver overlay --subnet 10.10.10.0/24 nw_swarm
Luego, conectaron los nodos Gitlab-CI y Swarm en términos de gestión remota de nodos CI: instalación de certificados, configuración de variables secretas y configuración del servicio Docker en el servidor de gestión. Este
artículo nos ahorró mucho tiempo.
Luego, agregamos trabajos para crear y destruir la pila en .gitlab-ci .yml.
Se agregaron algunos trabajos más a .gitlab-ci .yml ## staging stage deploy_staging: stage: testing before_script: - echo "override global 'before_script'" image: "REGISTRY:5000/docker:latest" environment: staging dependencies: [] variables: DOCKER_CERT_PATH: "/certs" DOCKER_HOST: tcp://10.50.173.107:2376 DOCKER_TLS_VERIFY: 1 CI_BIN_DEPENDENCIES_JOB: "release.centos.7" script: - mkdir -p $DOCKER_CERT_PATH - echo "$TLSCACERT" > $DOCKER_CERT_PATH/ca.pem - echo "$TLSCERT" > $DOCKER_CERT_PATH/cert.pem - echo "$TLSKEY" > $DOCKER_CERT_PATH/key.pem - docker stack deploy -c docker-compose.yml ${CI_ENVIRONMENT_NAME}_${CI_COMMIT_REF_NAME} --with-registry-auth - rm -rf $DOCKER_CERT_PATH when: manual ## stop staging stage stop_staging: stage: testing before_script: - echo "override global 'before_script'" image: "REGISTRY:5000/docker:latest" environment: staging dependencies: [] variables: DOCKER_CERT_PATH: "/certs" DOCKER_HOST: tcp://10.50.173.107:2376 DOCKER_TLS_VERIFY: 1 script: - mkdir -p $DOCKER_CERT_PATH - echo "$TLSCACERT" > $DOCKER_CERT_PATH/ca.pem - echo "$TLSCERT" > $DOCKER_CERT_PATH/cert.pem - echo "$TLSKEY" > $DOCKER_CERT_PATH/key.pem - docker stack rm ${CI_ENVIRONMENT_NAME}_${CI_COMMIT_REF_NAME} # TODO: need check that stopped when: manual
Del fragmento de código anterior, está claro que se han agregado dos botones (deploy_staging, stop_staging) a las canalizaciones que requieren intervención manual.

El nombre de la pila corresponde al nombre de la rama y esta singularidad debería ser suficiente. Los servicios en la pila reciben direcciones IP únicas y puertos, directorios, etc. estará aislado, pero lo mismo de una pila a otra (porque el archivo de configuración es el mismo para todas las pilas): esto es lo que logramos. Implementamos
la pila (cluster) usando
docker-compose.yml , que describe nuestro cluster.
docker-compose.yml --- version: '3' services: userprop: image: redis:alpine deploy: replicas: 1 placement: constraints: [node.id == kilqc94pi2upzvabttikrfr5d] restart_policy: condition: none networks: nw_swarm: celery_bcd: image: redis:alpine deploy: replicas: 1 placement: constraints: [node.id == kilqc94pi2upzvabttikrfr5d] restart_policy: condition: none networks: nw_swarm: schedulerdb: image: mariadb:latest environment: MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' MYSQL_DATABASE: schedulerdb MYSQL_USER: **** MYSQL_PASSWORD: **** command: ['--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci', '--explicit_defaults_for_timestamp=1'] deploy: replicas: 1 placement: constraints: [node.id == kilqc94pi2upzvabttikrfr5d] restart_policy: condition: none networks: nw_swarm: celerydb: image: mariadb:latest environment: MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' MYSQL_DATABASE: celerydb MYSQL_USER: **** MYSQL_PASSWORD: **** deploy: replicas: 1 placement: constraints: [node.id == kilqc94pi2upzvabttikrfr5d] restart_policy: condition: none networks: nw_swarm: cluster: image: $CENTOS7 environment: - CENTOS - CI_ENVIRONMENT_NAME - CI_API_V4_URL - CI_REPOSITORY_URL - CI_PROJECT_ID - CI_PROJECT_URL - CI_PROJECT_PATH - CI_PROJECT_NAME - CI_COMMIT_REF_NAME - CI_BIN_DEPENDENCIES_JOB command: > sudo -u myusername -H /bin/bash -c ". /etc/profile && mkdir -p /storage1/$CI_COMMIT_REF_NAME/$CI_PROJECT_NAME && cd /storage1/$CI_COMMIT_REF_NAME/$CI_PROJECT_NAME && git clone -b $CI_COMMIT_REF_NAME $CI_REPOSITORY_URL . && curl $CI_API_V4_URL/projects/$CI_PROJECT_ID/jobs/artifacts/$CI_COMMIT_REF_NAME/download?job=$CI_BIN_DEPENDENCIES_JOB -o artifacts.zip && unzip artifacts.zip ; cd /storage1/$CI_COMMIT_REF_NAME/$CI_PROJECT_NAME/scripts/deploy/ && python3 createconfig.py -s $CI_ENVIRONMENT_NAME && /bin/bash install_venv.sh -d -r ../../requirements.txt && python3 prepare_init.d.py && python3 deploy.py -s $CI_ENVIRONMENT_NAME" deploy: replicas: 1 placement: constraints: [node.id == kilqc94pi2upzvabttikrfr5d] restart_policy: condition: none tty: true stdin_open: true networks: nw_swarm: networks: nw_swarm: external: true
Aquí puede ver que los componentes están conectados por una red (nw_swarm) y son accesibles entre sí.
Los componentes del sistema (basados en redis, mysql) están separados del grupo común de componentes personalizados (los planes y los personalizados se dividen como servicios). La etapa de implementación de nuestro clúster se parece a la transferencia de CMD a nuestra imagen configurada de gran tamaño, y en su conjunto prácticamente no difiere de la implementación descrita en la Parte I. Destaco las diferencias:
- git clone ... - obtenemos los archivos necesarios para realizar una implementación (createconfig.py, install_venv.sh, etc.)
- curl ... && unzip ... - descargar y descomprimir artefactos de ensamblaje (utilidades compiladas)
Solo hay un problema que aún no se ha descrito: los navegadores de desarrollador no pueden acceder a los componentes que tienen una interfaz web. Resolvemos este problema usando proxy inverso, por lo tanto:
En .gitlab-ci.yml, después de implementar la pila del clúster, agregue la línea de implementación del equilibrador (que, al confirmar, solo actualiza su configuración (crea nuevos archivos de configuración nginx usando la plantilla: /etc/nginx/conf.d/${CI_COMMIT_REF_NAME►.conf) - ver código docker-compose-nginx.yml)
- docker stack deploy -c docker-compose-nginx.yml ${CI_ENVIRONMENT_NAME} --with-registry-auth
docker-compose-nginx.yml --- version: '3' services: nginx: image: nginx:latest environment: CI_COMMIT_REF_NAME: ${CI_COMMIT_REF_NAME} NGINX_CONFIG: |- server { listen 8080; server_name staging_${CI_COMMIT_REF_NAME}_cluster.dev; location / { proxy_pass http://staging_${CI_COMMIT_REF_NAME}_cluster:8080; } } server { listen 5555; server_name staging_${CI_COMMIT_REF_NAME}_cluster.dev; location / { proxy_pass http://staging_${CI_COMMIT_REF_NAME}_cluster:5555; } } volumes: - /tmp/staging/nginx:/etc/nginx/conf.d command: /bin/bash -c "echo -e \"$$NGINX_CONFIG\" > /etc/nginx/conf.d/${CI_COMMIT_REF_NAME}.conf; nginx -g \"daemon off;\"; /etc/init.d/nginx reload" ports: - 8080:8080 - 5555:5555 - 3000:3000 - 443:443 - 80:80 deploy: replicas: 1 placement: constraints: [node.id == kilqc94pi2upzvabttikrfr5d] restart_policy: condition: none networks: nw_swarm: networks: nw_swarm: external: true
En las computadoras de desarrollo, actualice / etc / hosts; registrar url a nginx:
10.50.173.106 staging_BRANCH-1831_cluster.dev
Por lo tanto, se implementó el despliegue de clústeres de etapas aisladas y los desarrolladores ahora pueden lanzarlos en
cualquier cantidad suficiente para probar sus tareas.
Planes adicionales:
- Separar nuestros componentes como servicios.
- Hacer para cada Dockerfile
- Detecta automáticamente nodos menos cargados en la pila
- Establecer nodos por patrón de nombre (en lugar de usar id como en el artículo)
- Agregar verificación de que la pila está destruida
- ...
Un agradecimiento especial por el
artículo .