Онлайн система за препоръки на видео съдържание, върху която работим, е затворена търговска разработка и технически представлява многостепенен клъстер от собствени и 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, …
- Потокът за събиране и тестване на компоненти е отделен за debug и release
Един от първите въпроси, които трябва да се решат в начален стадий, е как ще се извърши разгръщането на кастомизирани компоненти в някаква среда (CI/CD).
Външните компоненти решиха да бъдат инсталирани и обновявани систематично. Персонализираните приложения, разработвани на C++ или Python, могат да бъдат разгръщани по няколко начина. Сред тях, например: създаване на системни пакети, изпращане на пакетите в хранилище с компилирани образи и последваща инсталация на сървърите. По непозната причина беше избран друг начин, а именно: с помощта на CI се компилират изпълними файлове на приложения, създава се виртуална среда на проекта, инсталират се py-модули от requirements.txt и всички тези артефакти се изпращат заедно с конфигурации, скриптове и съпътстваща среда на приложенията на сървърите. След това приложенията се стартират от виртуален потребител без администраторски права.
За система CI/CD беше избрано Gitlab-CI. Полученият pipeline изглеждаше приблизително така:

Структурно gitlab-ci.yml изглеждаше по следния начин
---
variables:
# минималната версия на ЦПУ на сървърите, където се разгръща клъстера
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
## 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-овете е интересен по свой начин, няма да разказвам за тях, тъй като описанието на всеки от тях би отнело значително време и не е целта на статията. Ще обърна внимание само на това, че етапът на деплоймънт се състои от последователност от извиквания на скриптове:
- createconfig.py — създава файл settings.ini с настройки на компонентите в различна среда за последващ деплой (Preproduction, Production, Testing, …)
- install_venv.sh — създава виртуално окружение за py-компоненти в определена директория и го копира на отдалечени сървъри
- prepare_init.d.py — подготвя скриптове за стартиране и спиране на компонентите на база шаблон
- deploy.py — разпределя и рестартира новите компоненти
Времето минаваше. Етапите staging бяха заменени с preproduction и production. Подкрепата на продукта се разшири на още един дистрибутив (CentOS). Добавиха се още 5 мощни физически сървъра и десетина виртуални. А на разработчиците и тестировачите им ставаше все по-трудно да тестват задачите си в среда, близка до работното състояние. В този момент стана ясно, че не можем да се справим без него...
Част II

И така, нашият кластер представлява доста особеност система от няколко десетки отделни компоненти, които не са описани с Dockerfile-ове. Може да се конфигурира за деплой в определена среда само в общ план. Нашата задача е да деплоираме клъстера в staging-среда, за да го тестваме преди предрелизните тестове.
Теоретично, може да има няколко едновременно работещи клъстера: толкова, колкото задачи в завършено състояние или близко до завършване. Мощностите на сървърите, с които разполагаме, позволяват да стартираме няколко клъстера на всеки сървър. Всеки staging-клъстер трябва да бъде изолиран (без пресичане на портове, директории и т.н.).
Най-ценният ресурс е нашето време, а ние го имахме малко.
За по-бърз старт избрахме Docker Swarm заради простотата и гъвкавостта на архитектурата. Първото, което направихме, е да създадем мениджър и няколко нодове на отдалечените сървъри:
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
kilqc94pi2upzvabttikrfr5d nop-test-1 Готов Активен 19.03.2
jilwe56pl2zvabupryuosdj78 nop-test-2 Готов Активен 19.03.2
j5a4yz1kr2xke6b1ohoqlnbq5 * nop-test-3 Готов Активен Лидер 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), изискващи ръчно действие.

Името на стека съответства на името на клона и тази уникалност трябва да е достатъчна. Услугите в стека получават уникални IP адреси, а портовете, директориите и т.н. ще бъдат изолирани, но еднакви от стек на стек (тъй като конфигурационният файл е един и същ за всички стека) — това е, което се стремяхме да постигнем. Стекът (кластера) развиваме с помощта на docker-compose.yml, в който е описан нашият кластер.
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
Тук е видно, че компонентите са свързани в една мрежа (nw_swarm) и са достъпни помежду си.
Системните компоненти (на базата на redis, mysql) са отделени от общия пул на персонализираните компоненти (в плановете е и персонализирането им като услуги). Етапът на деплой на нашия клъстер изглежда като предаване на CMD в нашия един голям конфигуриран образ и в общи линии почти не се различава от деплоя, описан в Част I. Подчертавам разликите:
- git clone … — получаваме файловете, нужни за извършване на деплоя (createconfig.py, install_venv.sh и т.н.)
- curl… && unzip … — изтегляме и разархивираме артефактите от сборката (компилирани утилити)
Остъна само един все още необработен проблем: компонентите, които имат уеб интерфейс, не са достъпни от браузерите на разработчиците. Решаваме този проблем с помощта на reverse proxy, по следния начин:
В .gitlab-ci.yml след разполагането на стека на кластера добавяме реда за разполагане на балансировчика (който при комити само обновява своята конфигурация (създава нови конфигурационни файлове на nginx по шаблон: /etc/nginx/conf.d/${CI_COMMIT_REF_NAME}.conf) — вижте кода 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
