Despliegue de aplicaciones con Docker Swarm

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í:

Despliegue de aplicaciones con Docker Swarm
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:

  1. 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, ...)
  2. install_venv.sh — crea un entorno virtual para los componentes de py en un directorio específico y lo copia en los servidores remotos
  3. prepare_init.d.py — prepara los scripts de inicio y detención de los componentes basándose en una plantilla
  4. 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

Despliegue de aplicaciones con Docker Swarm

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 Windows 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.

Despliegue de aplicaciones con Docker Swarm
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 artículo.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster