Implementar 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 es un grupo de múltiples componentes de componentes propietarios y de código abierto. El propósito de escribir este artículo es describir la implementación del sistema de agrupación en clústeres Docker Swarm para un sitio de ensayo sin interrumpir el flujo de trabajo establecido de nuestros procesos en un tiempo limitado. La narración presentada a su atención se divide en dos partes. La primera parte describe CI/CD antes de usar docker swarm, y la segunda describe el proceso de su implementación. Aquellos que no estén interesados ​​en leer la primera parte pueden pasar con seguridad a la segunda.

Parte I

En el lejano, lejano año, era necesario configurar el proceso de CI / CD lo más rápido posible. Una de las condiciones era no usar Docker para despliegue componentes desarrollados por varias razones:

  • para un funcionamiento más fiable y estable de los componentes en producción (es decir, de hecho, el requisito de no utilizar la virtualización)
  • Los principales desarrolladores no querían trabajar con Docker (raro, pero así fue)
  • según las consideraciones ideológicas de la dirección de I+D

La infraestructura, la pila y los requisitos iniciales aproximados para MVP se presentaron de la siguiente manera:

  • 4 servidores Intel® X5650 con Debian (una máquina más poderosa está completamente desarrollada)
  • El desarrollo de componentes personalizados propios se lleva a cabo en C ++, Python3
  • Principales herramientas de terceros utilizadas: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
  • Canalizaciones para compilar y probar componentes por separado para depuración y lanzamiento

Una de las primeras preguntas que debe abordarse en la etapa inicial es cómo se implementarán los componentes personalizados en cualquier entorno (CI/CD).

Decidimos instalar componentes de terceros sistemáticamente y actualizarlos sistemáticamente. 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 construidas y luego instalarlos en los servidores. Por una razón desconocida, se eligió otro método, a saber: usando CI, se compilan los archivos ejecutables de la aplicación, se crea un entorno de proyecto virtual, se instalan módulos py desde requirements.txt, y todos estos artefactos se envían junto con configuraciones, scripts y el entorno de aplicación que acompaña a los servidores. A continuación, las aplicaciones se inician como un usuario virtual sin derechos de administrador.

Gitlab-CI fue elegido como el sistema CI/CD. La canalización resultante se parecía a esto:

Implementar aplicaciones con Docker Swarm
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 *ntStrictHostKeyChecking nonn" > ~/.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 ya se han instalado todos los paquetes de sistema necesarios y se han realizado otras configuraciones.

Aunque cada uno de estos guiones en los trabajos es interesante a su manera, pero por supuesto no hablaré de ellos, la descripción de cada uno de ellos llevará mucho tiempo y ese no es el propósito del artículo. Solo llamaré su atención sobre el hecho de que la etapa de implementación consiste en una secuencia de scripts de llamada:

  1. crearconfig.py - crea un archivo settings.ini con la configuración de los componentes en varios entornos para su posterior implementación (Preproducción, Producción, Pruebas, ...)
  2. instalar_venv.sh - crea un entorno virtual para componentes py en un directorio específico y lo copia en servidores remotos
  3. preparar_init.d.py — prepara secuencias de comandos de inicio y parada para el componente en función de la plantilla
  4. desplegar.py - descompone y reinicia nuevos componentes

Pasó el tiempo. La etapa de puesta en escena fue reemplazada por preproducción y producción. Se agregó soporte para el producto en una distribución más (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 probar sus tareas en un entorno más o menos cercano al estado de trabajo. En este momento, quedó claro que era imposible prescindir de él ...

Parte II

Implementar aplicaciones con Docker Swarm

Entonces, nuestro clúster es un sistema espectacular de un par de docenas de componentes separados que no se describen en Dockerfiles. Solo puede configurarlo para su implementación en un entorno específico en general. Nuestra tarea es implementar el clúster en un entorno de prueba para probarlo antes de la prueba previa al lanzamiento.

Teóricamente, puede haber varios clústeres ejecutándose simultáneamente: tantos como tareas en estado completado o cerca de completarse. Las capacidades de los servidores a nuestra disposición nos permiten ejecutar varios clústeres en cada servidor. Cada grupo de ensayo debe estar aislado (no debe haber intersección en puertos, directorios, etc.).

Nuestro recurso más valioso es nuestro tiempo, y no teníamos mucho de él.

Para un comienzo más rápido, elegimos Docker Swarm debido a su simplicidad y flexibilidad de arquitectura. Lo primero que hicimos fue crear un administrador y varios nodos en los servidores remotos:

$ 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, cree una red:


$ docker network create --driver overlay --subnet 10.10.10.0/24 nw_swarm

A continuación, vinculamos los nodos de Gitlab-CI y Swarm en términos de control remoto de nodos desde CI: instalación de certificados, configuración de variables secretas y configuración del servicio Docker en el servidor de control. Éste artículo nos ahorró mucho tiempo.

A continuación, agregamos trabajos de creación y destrucción de pilas a .gitlab-ci .yml.

Se han agregado 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

En el fragmento de código anterior, puede ver que se agregaron dos botones (deploy_staging, stop_staging) a Pipelines, lo que requiere una acción manual.

Implementar aplicaciones con Docker Swarm
El nombre de la pila coincide con el nombre de la rama y esta unicidad debería ser suficiente. Los servicios en la pila reciben direcciones IP únicas, 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), lo que queríamos. Desplegamos la pila (clúster) usando docker-compose.yml, que describe nuestro clúster.

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 están disponibles entre sí.

Los componentes del sistema (basados ​​en redis, mysql) están separados del grupo general de componentes personalizados (en los planes y los personalizados se dividen como servicios). La etapa de implementación de nuestro clúster parece pasar CMD a nuestra gran imagen configurada y, en general, prácticamente no difiere de la implementación descrita en la Parte I. Resaltaré las diferencias:

  • clonar git... - obtenga los archivos necesarios para implementar (createconfig.py, install_venv.sh, etc.)
  • rizar... && descomprimir... - descargar y descomprimir artefactos de compilación (utilidades compiladas)

Solo hay un problema aún no descrito: los componentes que tienen una interfaz web no son accesibles desde los navegadores de los desarrolladores. Resolvemos este problema usando proxy inverso, así:

En .gitlab-ci.yml, después de implementar la pila del clúster, agregamos la línea de implementación del balanceador (que, cuando se confirma, solo actualiza su configuración (crea nuevos archivos de configuración de nginx de acuerdo con la plantilla: /etc/nginx/conf. d/${CI_COMMIT_REF_NAME}.conf) - consulte el 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; prescribir url a nginx:

10.50.173.106 staging_BRANCH-1831_cluster.dev

Por lo tanto, se implementó la implementación de clústeres de prueba aislados y los desarrolladores ahora pueden ejecutarlos en cualquier cantidad suficiente para verificar sus tareas.

Planes futuros:

  • Separar nuestros componentes como servicios
  • Tener para cada Dockerfile
  • Detecta automáticamente los nodos menos cargados en la pila
  • Especificar nodos por patrón de nombre (en lugar de usar id como en el artículo)
  • Agregue un cheque que la pila se destruya
  • ...

gracias especiales por Artículo.

Fuente: habr.com

Añadir un comentario