Le système de recommandation de contenu vidéo en ligne sur lequel nous travaillons est un développement commercial fermé et est techniquement un cluster multi-composants de composants propriétaires et open source. Le but de la rédaction de cet article est de décrire la mise en œuvre du système de clustering docker swarm pour un site intermédiaire sans perturber le flux de travail établi de nos processus dans un temps limité. Le récit présenté à votre attention est divisé en deux parties. La première partie décrit le CI/CD avant d'utiliser docker swarm, et la seconde décrit le processus de sa mise en place. Ceux qui ne sont pas intéressés par la lecture de la première partie peuvent passer en toute sécurité à la seconde.
Partie I
De retour dans l'année lointaine, lointaine, il était nécessaire de mettre en place le processus CI / CD le plus rapidement possible. Une des conditions était de ne pas utiliser Docker pour le déploiement composants développés pour plusieurs raisons :
- pour un fonctionnement plus fiable et stable des composants en production (c'est-à-dire, en fait, l'obligation de ne pas utiliser la virtualisation)
- les principaux développeurs ne voulaient pas travailler avec Docker (bizarre, mais c'était comme ça)
- selon les considérations idéologiques de la direction R&D
L'infrastructure, la pile et les exigences initiales approximatives pour MVP ont été présentées comme suit :
- 4 serveurs Intel® X5650 avec Debian (une machine plus puissante est entièrement développée)
- Le développement de propres composants personnalisés est effectué en C ++, Python3
- Principaux outils tiers utilisés : Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
- Pipelines pour créer et tester des composants séparément pour le débogage et la publication
L'une des premières questions qui doit être abordée au stade initial est de savoir comment les composants personnalisés seront déployés dans n'importe quel environnement (CI / CD).
Nous avons décidé d'installer systématiquement des composants tiers et de les mettre à jour systématiquement. Les applications personnalisées développées en C++ ou Python peuvent être déployées de plusieurs manières. Parmi eux, par exemple : créer des packages système, les envoyer au référentiel d'images construites, puis les installer sur des serveurs. Pour une raison inconnue, une autre méthode a été choisie, à savoir : à l'aide de CI, les fichiers exécutables de l'application sont compilés, un environnement de projet virtuel est créé, les modules py sont installés à partir de requirements.txt, et tous ces artefacts sont envoyés avec les configurations, les scripts et le accompagnement de l'environnement applicatif vers les serveurs. Ensuite, les applications sont lancées en tant qu'utilisateur virtuel sans droits d'administrateur.
Gitlab-CI a été choisi comme système CI/CD. Le pipeline résultant ressemblait à ceci :
Structurellement, gitlab-ci.yml ressemblait à ceci
---
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
Il convient de noter que l'assemblage et les tests sont effectués sur sa propre image, où tous les packages système nécessaires ont déjà été installés et d'autres paramètres ont été définis.
Bien que chacun de ces scripts dans les métiers soit intéressant à sa manière, mais bien sûr je n'en parlerai pas, la description de chacun d'eux prendra beaucoup de temps et ce n'est pas le but de l'article. J'attire seulement votre attention sur le fait que l'étape de déploiement consiste en une séquence de scripts d'appel :
- créerconfig.py - crée un fichier settings.ini avec les paramètres des composants dans différents environnements pour un déploiement ultérieur (préproduction, production, test, ...)
- install_venv.sh - crée un environnement virtuel pour les composants py dans un répertoire spécifique et le copie sur des serveurs distants
- préparer_init.d.py — prépare les scripts start-stop pour le composant en fonction du modèle
- déployer.py - décompose et redémarre de nouveaux composants
Le temps passait. L'étape de la mise en scène a été remplacée par la préproduction et la production. Ajout de la prise en charge du produit sur une distribution supplémentaire (CentOS). Ajout de 5 serveurs physiques plus puissants et d'une dizaine de serveurs virtuels. Et il devenait de plus en plus difficile pour les développeurs et les testeurs de tester leurs tâches dans un environnement plus ou moins proche de l'état de fonctionnement. À ce moment-là, il est devenu clair qu'il était impossible de se passer de lui ...
Partie II
Ainsi, notre cluster est un système spectaculaire de quelques dizaines de composants séparés qui ne sont pas décrits par Dockerfiles. Vous pouvez uniquement le configurer pour un déploiement dans un environnement spécifique en général. Notre tâche consiste à déployer le cluster dans un environnement intermédiaire pour le tester avant les tests de pré-version.
Théoriquement, plusieurs clusters peuvent s'exécuter simultanément : autant qu'il y a de tâches à l'état terminé ou proche de l'achèvement. Les capacités des serveurs à notre disposition nous permettent de faire tourner plusieurs clusters sur chaque serveur. Chaque cluster intermédiaire doit être isolé (il ne doit y avoir aucune intersection dans les ports, les répertoires, etc.).
Notre ressource la plus précieuse est notre temps, et nous n'en avions pas beaucoup.
Pour un démarrage plus rapide, nous avons choisi Docker Swarm en raison de sa simplicité et de la flexibilité de son architecture. La première chose que nous avons faite a été de créer un gestionnaire et plusieurs nœuds sur les serveurs distants :
$ 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
Ensuite, créez un réseau :
$ docker network create --driver overlay --subnet 10.10.10.0/24 nw_swarm
Ensuite, nous avons connecté les nœuds Gitlab-CI et Swarm en termes de contrôle à distance des nœuds depuis CI : installation de certificats, définition de variables secrètes et configuration du service Docker sur le serveur de contrôle. Celui-ci
Ensuite, nous avons ajouté des tâches de création et de destruction de pile à .gitlab-ci .yml.
Quelques tâches supplémentaires ont été ajoutées à .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
À partir de l'extrait de code ci-dessus, vous pouvez voir que deux boutons (deploy_staging, stop_staging) ont été ajoutés aux pipelines, nécessitant une action manuelle.
Le nom de la pile correspond au nom de la branche et cette unicité devrait être suffisante. Les services de la pile reçoivent des adresses IP uniques, des ports, des répertoires, etc. sera isolé, mais identique d'une pile à l'autre (car le fichier de configuration est le même pour toutes les piles) - ce que nous voulions. Nous déployons la pile (cluster) en utilisant docker-compose.yml, qui décrit notre 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
Ici, vous pouvez voir que les composants sont connectés par un réseau (nw_swarm) et sont disponibles les uns pour les autres.
Les composants système (basés sur redis, mysql) sont séparés du pool général de composants personnalisés (dans les plans et les composants personnalisés sont divisés en services). L'étape de déploiement de notre cluster ressemble à la transmission de CMD dans notre seule grande image configurée et, en général, ne diffère pratiquement pas du déploiement décrit dans la partie I. Je soulignerai les différences :
- cloner git... - obtenir les fichiers nécessaires au déploiement (createconfig.py, install_venv.sh, etc.)
- enrouler... && décompresser... - télécharger et décompresser les artefacts de construction (utilitaires compilés)
Il n'y a qu'un seul problème encore non décrit : les composants qui ont une interface Web ne sont pas accessibles depuis les navigateurs des développeurs. Nous résolvons ce problème en utilisant un proxy inverse, ainsi :
Dans .gitlab-ci.yml, après avoir déployé la pile du cluster, nous ajoutons la ligne de déploiement de l'équilibreur (qui, lors des validations, ne fait que mettre à jour sa configuration (crée de nouveaux fichiers de configuration nginx selon le modèle : /etc/nginx/conf. d/${CI_COMMIT_REF_NAME}.conf) - voir le code 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
Sur les ordinateurs de développement, mettez à jour /etc/hosts ; prescrire l'url à nginx :
10.50.173.106 staging_BRANCH-1831_cluster.dev
Ainsi, le déploiement de clusters de staging isolés a été implémenté et les développeurs peuvent désormais les exécuter en nombre suffisant pour vérifier leurs tâches.
Plans futurs:
- Séparer nos composants en tant que services
- Avoir pour chaque Dockerfile
- Détecter automatiquement les nœuds les moins chargés dans la pile
- Spécifiez les nœuds par modèle de nom (plutôt que d'utiliser l'identifiant comme dans l'article)
- Ajouter une vérification que la pile est détruite
- ...