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 consiste en un clúster multicomponente de componentes propios y de código abierto. El objetivo de este artículo es describir la implementación de un sistema de clústeres docker swarm en un entorno de staging, sin alterar el flujo de trabajo existente de nuestros procesos en un tiempo limitado. La narrativa que se presenta está dividida en dos partes. La primera parte describe CI/CD antes de utilizar docker swarm, y la segunda, el proceso de su implementación. Quien no esté interesado en leer la primera parte puede pasar directamente a la segunda.
Parte I
En un año muy lejano, era necesario configurar el proceso CI/CD lo más rápido posible. Una de las condiciones era no usar Docker para el despliegue de los componentes en desarrollo por varias razones:
- para un funcionamiento más fiable y estable de los componentes en producción (es decir, esencialmente un requisito de no usar virtualización)
- los principales desarrolladores no querían trabajar con Docker (extraño, pero así era)
- por razones ideológicas del liderazgo de R&D
La infraestructura, el stack y los requisitos iniciales aproximados para el MVP eran los siguientes:
- 4 servidores Intel® X5650 con Debian (una máquina más potente completamente dedicada al desarrollo)
- El desarrollo de componentes personalizados se realiza en C++, Python3
- Los principales 3rd party utilizados son: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
- Pipelines de construcción y prueba de componentes por separado para debug y release
Una de las primeras preguntas que debe resolverse en la etapa inicial es cómo se llevará a cabo el despliegue de los componentes personalizados en algún entorno (CI/CD).
Los componentes externos decidieron instalarse y actualizarse de manera sistemática. Las aplicaciones personalizadas, desarrolladas en C++ o Python, pueden desplegarse de varias formas. Entre ellas, por ejemplo: la creación de paquetes del sistema, su envío a un repositorio de imágenes compiladas y su posterior instalación en los servidores. Por una razón desconocida, se eligió otro método, a saber: mediante CI se compilan archivos ejecutables de las aplicaciones, se crea un entorno virtual del proyecto, se instalan los módulos de Python de requirements.txt y todos estos artefactos se envían junto con las configuraciones, scripts y el entorno correspondiente de las aplicaciones a los servidores. Luego, las aplicaciones se inician desde un usuario virtual sin privilegios de administrador.
Se eligió Gitlab-CI como sistema CI/CD. El pipeline resultante se veía aproximadamente así:

Estructuralmente, gitlab-ci.yml se veía de la siguiente manera
---
variables:
# versión mínima de la CPU en los servidores donde se despliega el clúster
CMAKE_CPUTYPE: "westmere"
DEBIAN: "MYREGISTRY:5000/debian:latest"
before_script:
- eval $(ssh-agent -s)
- ssh-add ~/.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
## etapa de prueba
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
## etapa de staging
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 mencionar que la construcción y las pruebas se realizan en su propia imagen, donde ya están instalados todos los paquetes del sistema necesarios y se han realizado otras configuraciones.
Aunque cada uno de estos scripts en los trabajos es interesante a su manera, no voy a hablar de ellos, ya que describir cada uno llevaría un tiempo considerable y no es el objetivo de este artículo. Solo quiero mencionar que la etapa de despliegue consiste en una secuencia de llamadas a scripts:
- createconfig.py — crea el archivo settings.ini con la configuración de componentes en diferentes entornos para el posterior despliegue (Preproducción, Producción, Pruebas, ...)
- install_venv.sh — crea un entorno virtual para los componentes de py en un directorio específico y lo copia en los servidores remotos
- prepare_init.d.py — prepara los scripts de inicio y detención de los componentes basándose en una plantilla
- deploy.py — distribuye y reinicia los nuevos componentes
Pasó el tiempo. La etapa de staging fue reemplazada por preproducción y producción. Se añadió soporte para el producto en otra distribución (CentOS). Se agregaron 5 servidores físicos potentes y una docena de virtuales. Y a los desarrolladores y testers les resultaba cada vez más difícil probar sus tareas en un entorno que estuviera más o menos cercano al estado operativo. En ese momento quedó claro que era imposible prescindir de él...
Parte II

Así que nuestro clúster es un espectáculo: un sistema de un par de docenas de componentes independientes, no descritos con Dockerfiles. Configurarlo para el despliegue en un entorno específico solo se puede hacer en su totalidad. Nuestra tarea es desplegar el clúster en un entorno de staging para probarlo antes de las pruebas de pre-lanzamiento.
Teóricamente, puede haber varios clústeres funcionando al mismo tiempo: tantos como tareas en estado terminado o cercano a la finalización. Los recursos disponibles en nuestros servidores permiten ejecutar varios clústeres en cada servidor. Cada clúster de staging debe estar aislado (no debe haber intersecciones de puertos, directorios, etc.).
El recurso más valioso es nuestro tiempo, y teníamos poco de él.
Para un inicio más rápido, elegimos Docker Swarm debido a su simplicidad y flexibilidad arquitectónica. Lo primero que hicimos fue crear un gestor y varios nodos en los servidores remotos:
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
kilqc94pi2upzvabttikrfr5d nop-test-1 Listo Activo 19.03.2
jilwe56pl2zvabupryuosdj78 nop-test-2 Listo Activo 19.03.2
j5a4yz1kr2xke6b1ohoqlnbq5 * nop-test-3 Listo Activo Líder 19.03.2
A continuación, se creó la red:
$ docker network create --driver overlay --subnet 10.10.10.0/24 nw_swarm
A continuación, vinculamos Gitlab-CI y los nodos de Swarm para la gestión remota de nodos desde CI: instalación de certificados, configuración de variables secretas, así como la configuración del servicio Docker en el servidor de gestión. Esto nos ahorró mucho tiempo.
A continuación, agregamos trabajos para crear y destruir el stack en .gitlab-ci.yml.
Se agregaron varios trabajos más en .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 se puede ver que en Pipelines se añadieron dos botones (deploy_staging, stop_staging), que requieren intervención manual.

El nombre del stack corresponde al nombre de la rama y esta unicidad debería ser suficiente. Los servicios en el stack obtienen direcciones IP únicas, y los puertos, directorios, etc. serán aislados, pero idénticos de stack a stack (ya que el archivo de configuración es el mismo para todos los stacks) — esto es lo que buscábamos. Desplegamos el stack (cluster) usando docker-compose.yml, en el que se 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í se puede ver que los componentes están unidos por una sola red (nw_swarm) y son accesibles entre sí.
Los componentes del sistema (basados en redis, mysql) están separados del pool general de componentes personalizados (se planea dividir los personalizados como servicios). La etapa de despliegue de nuestro clúster se parece a la transmisión de CMD en nuestra gran imagen configurada y, en general, no se diferencia prácticamente del despliegue descrito en la Parte I. Subrayo las diferencias:
- git clone … — obtenemos los archivos necesarios para realizar el despliegue (createconfig.py, install_venv.sh, etc.)
- curl… && unzip … — descargamos y descomprimimos los artefactos de la compilación (utilidades compiladas)
Solo queda un problema aún no descrito: los componentes con interfaz web no son accesibles desde los navegadores de los desarrolladores. Estamos resolviendo este problema mediante un reverse proxy, de la siguiente manera:
En .gitlab-ci.yml, después del despliegue del stack del clúster, añadimos la línea para el despliegue del balanceador (que al hacer commits, solo actualiza su configuración (crea nuevos archivos de configuración de nginx según 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 los desarrolladores actualizamos /etc/hosts; escribimos la url hasta nginx:
10.50.173.106 staging_BRANCH-1831_cluster.dev
Así que el despliegue de clústeres staging aislados está implementado y ahora los desarrolladores pueden ejecutarlos en cualquier cantidad suficiente para verificar sus tareas.
Planes futuros:
- Separar nuestros componentes como servicios
- Crear un Dockerfile para cada uno
- Determinar automáticamente los nodos menos cargados en el stack
- Definir nodos según una plantilla de nombre (en lugar de usar el id como en el artículo)
- Agregar una verificación de que el stack ha sido destruido
- …
Agradecimientos especiales a .
Fuente: habr.com
