Implementa aplicacións con Docker Swarm

O sistema de recomendación de contidos de vídeo en liña no que estamos a traballar é un desenvolvemento comercial pechado e, tecnicamente, é un clúster multicomponente de compoñentes propietarios e de código aberto. O propósito de escribir este artigo é describir a implementación do sistema de agrupación docker swarm para un sitio de proba sen interromper o fluxo de traballo establecido dos nosos procesos nun tempo limitado. A narración que se presenta á súa atención divídese en dúas partes. A primeira parte describe CI/CD antes de usar docker swarm, e a segunda describe o proceso da súa implementación. Os que non estean interesados ​​en ler a primeira parte poden pasar á segunda con seguridade.

Primeira parte

De volta ao ano afastado e afastado, foi necesario configurar o proceso CI/CD o máis rápido posible. Unha das condicións era non usar Docker para o despregamento compoñentes desenvolvidos por varias razóns:

  • para un funcionamento máis fiable e estable dos compoñentes en Produción (é dicir, o requisito de non usar a virtualización)
  • os principais desenvolvedores non querían traballar con Docker (raro, pero así foi)
  • segundo as consideracións ideolóxicas da xestión da I+D+i

A infraestrutura, a pila e os requisitos iniciais aproximados para MVP presentáronse do seguinte xeito:

  • 4 servidores Intel® X5650 con Debian (unha máquina máis potente está totalmente desenvolvida)
  • O desenvolvemento de compoñentes personalizados propios realízase en C++, Python3
  • Principais ferramentas de terceiros utilizadas: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
  • Canalizacións para construír e probar compoñentes por separado para a depuración e a liberación

Unha das primeiras preguntas que hai que abordar na fase inicial é como se implementarán os compoñentes personalizados en calquera ambiente (CI/CD).

Decidimos instalar compoñentes de terceiros sistemáticamente e actualizalos sistemáticamente. As aplicacións personalizadas desenvolvidas en C++ ou Python pódense implantar de varias maneiras. Entre elas, por exemplo: crear paquetes do sistema, envialos ao repositorio de imaxes construídas e despois instalalos en servidores. Por unha razón descoñecida, escolleuse outro método, a saber: mediante CI, compílanse ficheiros executables de aplicacións, créase un entorno de proxecto virtual, instálanse módulos py desde requirements.txt e todos estes artefactos envíanse xunto con configuracións, scripts e ambiente de aplicación que acompaña aos servidores. A continuación, lánzanse as aplicacións como un usuario virtual sen dereitos de administrador.

Elixiuse Gitlab-CI como sistema CI/CD. A canalización resultante parecía algo así:

Implementa aplicacións con Docker Swarm
Estruturalmente, gitlab-ci.yml tiña este aspecto

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

Cabe destacar que a montaxe e probas realízanse na súa propia imaxe, onde xa se instalaron todos os paquetes de sistema necesarios e se realizaron outras configuracións.

Aínda que cada un destes guións nos traballos é interesante ao seu xeito, pero por suposto non falarei deles.A descrición de cada un deles levará moito tempo e este non é o propósito do artigo. Só chamarei a súa atención sobre o feito de que a fase de implantación consiste nunha secuencia de scripts de chamada:

  1. createconfig.py - crea un ficheiro settings.ini con axustes de compoñentes en varios ambientes para a súa posterior implantación (preprodución, produción, probas, ...)
  2. install_venv.sh - crea un ambiente virtual para compoñentes py nun directorio específico e cópiao a servidores remotos
  3. prepare_init.d.py — prepara guións de inicio e parada para o compoñente baseándose no modelo
  4. despregar.py - descompón e reinicia novos compoñentes

O tempo pasou. A etapa de posta en escena foi substituída pola preprodución e produción. Engadido soporte para o produto nunha distribución máis (CentOS). Engadíronse 5 servidores físicos máis potentes e unha ducia de virtuais. E facíase cada vez máis difícil para os desenvolvedores e probadores probar as súas tarefas nun ambiente máis ou menos próximo ao estado de traballo. Neste momento, quedou claro que era imposible prescindir del...

Parte II

Implementa aplicacións con Docker Swarm

Polo tanto, o noso clúster é un sistema espectacular dun par de ducias de compoñentes separados que non están descritos por Dockerfiles. Só pode configuralo para a súa implantación nun ambiente específico en xeral. A nosa tarefa é implantar o clúster nun ambiente de proba para probalo antes da proba previa ao lanzamento.

Teoricamente, pode haber varios clústeres en execución simultáneamente: tantos como tarefas haxa en estado completado ou próximas á súa finalización. As capacidades dos servidores á nosa disposición permítennos executar varios clústeres en cada servidor. Cada clúster de proba debe estar illado (non debe haber intersección en portos, directorios, etc.).

O noso recurso máis valioso é o noso tempo, e non tiñamos moito del.

Para comezar máis rápido, escollemos Docker Swarm debido á súa sinxeleza e flexibilidade de arquitectura. O primeiro que fixemos foi crear un xestor e varios nodos nos 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, crea unha rede:


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

A continuación, conectamos os nós Gitlab-CI e Swarm en termos de control remoto de nós desde CI: instalación de certificados, configuración de variables secretas e configuración do servizo Docker no servidor de control. Este artigo aforrounos moito tempo.

A continuación, engadimos traballos de creación e destrución de pilas a .gitlab-ci .yml.

Engadíronse algúns traballos máis 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

No fragmento de código anterior, podes ver que se engadiron dous botóns (deploy_staging, stop_staging) a Pipelines, que requiren unha acción manual.

Implementa aplicacións con Docker Swarm
O nome da pila coincide co nome da rama e esta singularidade debería ser suficiente. Os servizos da pila reciben enderezos IP únicos e portos, directorios, etc. estará illado, pero o mesmo de pila en pila (porque o ficheiro de configuración é o mesmo para todas as pilas) - o que queriamos. Implementamos a pila (clúster) usando docker-compose.yml, que describe o noso 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í podes ver que os compoñentes están conectados por unha rede (nw_swarm) e están dispoñibles entre si.

Os compoñentes do sistema (baseados en redis, mysql) están separados do conxunto xeral de compoñentes personalizados (nos plans e os personalizados divídense como servizos). A fase de implantación do noso clúster parece pasar CMD á nosa imaxe grande configurada e, en xeral, practicamente non difire da implantación descrita na Parte I. Destacarei as diferenzas:

  • clon git... - obter os ficheiros necesarios para implementar (createconfig.py, install_venv.sh, etc.)
  • enrolar... && descomprimir... - descargar e descomprimir artefactos de compilación (utilidades compiladas)

Só hai un problema aínda sen describir: os compoñentes que teñen unha interface web non son accesibles desde os navegadores dos desenvolvedores. Resolvemos este problema usando un proxy inverso, así:

En .gitlab-ci.yml, despois de despregar a pila de clúster, engade unha liña para despregar o equilibrador (que, cando se compromete, só actualiza a súa configuración (crea novos ficheiros de configuración de nginx segundo o modelo: /etc/nginx/conf.d) /${CI_COMMIT_REF_NAME}.conf) - ver o 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

Nos ordenadores de desenvolvemento, actualice /etc/hosts; prescribe URL para nginx:

10.50.173.106 staging_BRANCH-1831_cluster.dev

Así, implantouse o despregamento de clústeres illados e os desenvolvedores agora poden executalos en calquera número suficiente para comprobar as súas tarefas.

Plans futuros:

  • Separe os nosos compoñentes como servizos
  • Teña para cada Dockerfile
  • Detecta automaticamente os nós menos cargados na pila
  • Especifique os nós polo patrón de nome (en lugar de usar id como no artigo)
  • Engade unha comprobación de que a pila está destruída
  • ...

Agradecemento especial para un artigo.

Fonte: www.habr.com

Engadir un comentario