Deploy додатків за допомогою Docker Swarm

Онлайн рекомендаційна система відео-контенту, над якою ми працюємо, є закритою комерційною розробкою та технічно є багатокомпонентним кластером з власних та open source компонентів. Метою написання цієї статті є опис впровадження системи кластеризації docker swarm під staging-майданчик, не порушуючи сформований workflow наших процесів в умовах обмеженого часу. Представлена ​​до вашої уваги розповідь поділена на дві частини. Перша частина визначає CI/CD до використання docker swarm, а друга - процес його застосування. Хто не зацікавлений у читанні першої частини може сміливо переходити до другої.

частина I

У далекому-далекому році потрібно якнайшвидше налаштувати процес CI/CD. Однією з умов було не використовувати Docker для деплою компонентів, що розробляються, з кількох причин:

  • для більш надійної та стабільної роботи компонент у Production (тобто по суті вимога не використовувати віртуалізацію)
  • провідні розробники не хотіли працювати з Docker (дивно, але було саме так)
  • з ідейних міркувань керівництва R&D

Інфраструктура, стек та приблизні вихідні вимоги для MVP були такими:

  • 4 сервери Intel® X5650 з Debian (одна потужніша машина повністю під розробку)
  • Розробка власних кастомних компонентів ведеться на C++, Python3
  • Основні 3rdparty-використовуються засоби: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
  • Pipelines складання та тестування компонент окремо для debug та release

Одним із перших питань, яке потрібно вирішити на початковій стадії, це яким чином буде проводитися розгортання кастомних компонентів у якомусь оточенні (CI/CD).

Сторонні компоненти вирішили ставити системно та оновлювати їх системно. Кастомні програми, що розробляються на C++ або Python, розгортати можна кількома способами. Серед них, наприклад: створення системних пакетів, відправлення їх до репозиторій зібраних образів та їх подальше встановлення на серверах. З невідомої вже причини був обраний інший спосіб, а саме: за допомогою CI компілюються файли додатків, що виконуються, створюється віртуальне оточення проекту, встановлюються py-модулі з requirements.txt і всі ці артефакти відправляються разом з конфігами, скриптами і супутнім оточенням додатків на сервери. Далі здійснюється запуск програм від віртуального користувача без прав адміністратора.

Як система CI/CD був обраний Gitlab-CI. Отриманий pipeline виглядав приблизно так:

Deploy додатків за допомогою Docker Swarm
Структурно gitlab-ci.yml виглядав так

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

Варто зауважити, що складання та тестування проводиться на власному образі, де вже встановлені всі необхідні системні пакети та виконані інші налаштування.

Хоча кожен із цих скриптів у job-ах цікавий по-своєму, але розповідати про них звичайно ж не буду опис кожного з них займе значний час і не в цьому мета статті. Зверну лише увагу, що стадія деплою складається з послідовності виклику скриптів:

  1. createconfig.py — створює файл settings.ini з налаштуваннями компонентів у різному оточенні для подальшого деплою (Preproduction, Production, Testing, …)
  2. install_venv.sh — створює віртуальне оточення для py-компонент у певній директорії та копіює його на віддалені сервери
  3. prepare_init.d.py — готує сркіпти старту-стопу компонент на основі шаблону
  4. deploy.py — розкладає та перезапускає нові компоненти

Ішов час. Стадію staging замінили preproduction та production. Додалася підтримка продукту ще одному дитрибутиві (CentOS). Додалося ще 5 потужних фізичних серверів та десяток віртуальних. А розробникам та тестувальникам ставало дедалі складніше обкатувати свої завдання на оточенні більш-менш наближеному до робочого стану. В цей час стало зрозуміло, що неможливо обійтися без нього.

частина II

Deploy додатків за допомогою Docker Swarm

Отже, наш кластер представляє собою те ще видовище систему з пари десятків окремих компонентів, не описаних Dockerfile-ами. Налаштувати його для деплою на певне оточення можна лише загалом. Наше завдання полягає в тому, щоб деплоїти кластер у staging-оточення для обкатки його перед передрелізним тестуванням.

Теоретично, одночасно працюючих кластерів може бути кілька: стільки скільки завдань у завершеному стані чи близькому до завершення. Потужності, що є у нашому розпорядженні серверів, дозволяють запускати кілька кластерів кожному сервері. Кожен staging-кластер повинен бути ізольований (не повинно бути перетину портами, директоріями тощо).

Найцінніший ресурс – це наш час, а його у нас було небагато.

Для швидшого старту вибрали Docker Swarm в силу його простоти та гнучкості архітектури. Перше, що ми зробили, це створили на віддалених серверах менеджера і кілька нод:

$ 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

Далі, створили мережу:


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

Далі, зв'язали Gitlab-CI та ноди Swarm у частині віддаленого управління нодами з CI: встановлення сертифікатів, налаштування секретних змінних, а також налаштування сервісу Docker на сервері, що управляє. Ось ця стаття нам сильно заощадила час.

Далі, додали job'и створення та знищення стеку в .gitlab-ci .yml.

У .gitlab-ci .yml додалося ще кілька job

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

З вищенаведеного фрагмента коду видно, що Pipelines додалися дві кнопки (deploy_staging, stop_staging), що вимагають ручного впливу.

Deploy додатків за допомогою Docker Swarm
Ім'я стека відповідає імені гілки і цієї унікальності має бути достатньо. Сервіси в стеку отримують унікальні IP-адреси, а порти, директорії тощо. будуть ізольованими, але однаковими від стеку до стеку (бо конфігураційний файл однаковий для всіх стеків) — те, чого ми й домагалися. Стек (кластер) ми розгортаємо за допомогою докер-компост.імл, де описано наш кластер.

докер-компост.імл

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

Тут видно, що компоненти об'єднані однією мережею (nw_swarm) та доступні один одному.

Системні компоненти (на основі redis, mysql) відокремлені від загального пулу кастомних компонентів (у планах і кастомні розділити як сервіси). Стадія деплою нашого кластера виглядає як передача CMD в наш один великий налаштований image і в цілому практично не відрізняється від деплою, описаного в Частині I. Підкреслю відмінності:

  • git clone … - Отримуємо файли, необхідні, щоб зробити деплой (createconfig.py, install_venv.sh і т.п.)
  • curl… && unzip … - Скачуємо і розархівуємо артефакти складання (компільовані утиліти)

Залишилася тільки одна поки неописана проблема: компоненти, які мають веб-інтерфейс, не доступні з браузерів розробників. Ми цю проблему вирішуємо за допомогою reverse proxy, таким чином:

У .gitlab-ci.yml після деплою стеку кластера додаємо рядок деплою балансувальника (який при комітах тільки оновлює свою конфігурацію (створює нові конфігураційні файли nginx за шаблоном: /etc/nginx/conf.d/${CI_COMMIT_REF_NAME). див. код 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

На комп'ютерах розробників оновлюємо /etc/hosts; прописуємо url до nginx:

10.50.173.106 staging_BRANCH-1831_cluster.dev

Отже, деплой ізольованих staging-кластерів реалізований і розробники тепер можуть запускати їх у будь-якій достатній для перевірки своїх завдань кількості.

Подальші плани:

  • Розділити наші компоненти як послуги
  • Завести для кожного Dockerfile
  • Автоматично визначати менш завантажені ноди у стеку
  • Задавати ноди за шаблоном імені (а не використовувати id як у статті)
  • Додати перевірку, що стек знищено
  • ...

Окрема подяка за статтю.

Джерело: habr.com

Додати коментар або відгук