Das Online-Empfehlungssystem für Video-Inhalte, an dem wir arbeiten, ist ein geschlossenes kommerzielles Projekt und stellt technisch einen mehrkomponentigen Cluster aus proprietären und Open-Source-Komponenten dar. Ziel dieses Artikels ist es, die Implementierung eines Docker Swarm-Clustering-Systems für die Staging-Umgebung zu beschreiben, ohne den bestehenden Workflow unserer Prozesse unter zeitlichen Einschränkungen zu beeinträchtigen. Die Ihnen präsentierte Erzählung ist in zwei Teile gegliedert. Der erste Teil beschreibt CI/CD vor der Nutzung von Docker Swarm, während der zweite den Prozess der Implementierung behandelt. Wer an der ersten Teil nicht interessiert ist, kann direkt zum zweiten übergehen.
Teil I
Im weit entfernten Jahr war es notwendig, den CI/CD-Prozess so schnell wie möglich einzurichten. Eine der Bedingungen war, Docker nicht zu verwenden für die Bereitstellung der entwickelten Komponenten aus mehreren Gründen:
- um eine zuverlässigere und stabilere Funktion der Komponenten in der Produktion zu gewährleisten (d.h. im Grunde die Anforderung, Virtualisierung nicht zu verwenden)
- die leitenden Entwickler wollten nicht mit Docker arbeiten (seltsam, aber es war so)
- aus ideologischen Gründen der Geschäftsführung der F&E
Die Infrastruktur, der Stack und die beispielhaften Anforderungen für ein MVP wurden wie folgt dargestellt:
- 4 Server Intel® X5650 mit Debian (ein leistungsstärkerer Server vollständig für die Entwicklung)
- Die Entwicklung eigener maßgeschneiderter Komponenten erfolgt in C++, Python3.
- Die wichtigsten 3rd Party-Tools, die verwendet werden: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
- Build- und Test-Pipelines für Komponenten separat für Debug und Release.
Eine der ersten Fragen, die in der Anfangsphase geklärt werden muss, ist, wie die Bereitstellung der maßgeschneiderten Komponenten in einer Umgebung (CI/CD) erfolgen wird.
Drittanbieter-Komponenten werden systematisch installiert und aktualisiert. Für benutzerdefinierte Anwendungen, die in C++ oder Python entwickelt werden, gibt es mehrere Bereitstellungsmöglichkeiten. Dazu gehört beispielsweise: die Erstellung von Systempaketen, das Hochladen in ein Repository für zusammengebaute Images und die anschließende Installation auf Servern. Aus nicht mehr nachvollziehbaren Gründen wurde jedoch ein anderer Ansatz gewählt: Mit Hilfe von CI werden die ausführbaren Dateien der Anwendungen kompiliert, eine virtuelle Projektumgebung erstellt, die py-Module aus der requirements.txt installiert und all diese Artefakte zusammen mit Konfigurationen, Skripten und der dazugehörigen Anwendungsumgebung auf die Server übertragen. Anschließend werden die Anwendungen von einem virtuellen Benutzer ohne Administratorrechte gestartet.
Als CI/CD-System wurde Gitlab-CI ausgewählt. Die resultierende Pipeline sah ungefähr so aus:

Strukturell sah die gitlab-ci.yml wie folgt aus
---
variablen:
# minimale CPU-Version auf Servern, auf denen der Cluster bereitgestellt wird
CMAKE_CPUTYPE: "westmere"
DEBIAN: "MYREGISTRY:5000/debian:latest"
before_script:
- eval $(ssh-agent -s)
- ssh-add ~/.ssh/config
stufen:
- build
- testing
- deploy
debug.debian:
stufe: build
bild: $DEBIAN
skript:
- cd builds/release && ./build.sh
pfade:
- bin/
- builds/release/bin/
wann: immer
release.debian:
stufe: build
bild: $DEBIAN
skript:
- cd builds/release && ./build.sh
pfade:
- bin/
- builds/release/bin/
wann: immer
## Teststufe
tests.codestyle:
stufe: testing
bild: $DEBIAN
abhängigkeiten:
- release.debian
skript:
- /bin/bash run_tests.sh -t codestyle -b "${CI_COMMIT_REF_NAME}_codestyle"
tests.debug.debian:
stufe: testing
bild: $DEBIAN
abhängigkeiten:
- debug.debian
skript:
- /bin/bash run_tests.sh -e codestyle/test_pylint.py -b "${CI_COMMIT_REF_NAME}_debian_debug"
artefakte:
pfade:
- run_tests/username/
wann: immer
ablaufen_in: 1 Woche
tests.release.debian:
stufe: testing
bild: $DEBIAN
abhängigkeiten:
- release.debian
skript:
- /bin/bash run_tests.sh -e codestyle/test_pylint.py -b "${CI_COMMIT_REF_NAME}_debian_release"
artefakte:
pfade:
- run_tests/username/
wann: immer
ablaufen_in: 1 Woche
## Staging-Stufe
deploy_staging:
stufe: deploy
umgebung: staging
bild: $DEBIAN
abhängigkeiten:
- release.debian
skript:
- 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
wann: manuell
Es ist wichtig zu beachten, dass die Zusammenstellung und das Testen in einem eigenen Image erfolgt, in dem bereits alle notwendigen Systempakete installiert sind und weitere Einstellungen vorgenommen wurden.
Obwohl jedes dieser Skripte in den Jobs auf seine Weise interessant ist, werde ich nicht näher auf sie eingehen, da die Beschreibung jedes einzelnen erheblich Zeit in Anspruch nehmen würde und das nicht das Ziel des Artikels ist. Ich möchte lediglich darauf hinweisen, dass die Deployment-Phase aus einer Reihe von Skripterufen besteht:
- createconfig.py — erstellt die Datei settings.ini mit den Konfigurationen der Komponenten in verschiedenen Umgebungen für das anschließende Deployment (Preproduction, Production, Testing, …)
- install_venv.sh — erstellt eine virtuelle Umgebung für Python-Komponenten in einem bestimmten Verzeichnis und kopiert sie auf die Remote-Server
- prepare_init.d.py — bereitet die Start- und Stoppskripte der Komponenten auf Grundlage einer Vorlage vor
- deploy.py — verteilt und startet die neuen Komponenten neu
Die Zeit verging. Die Staging-Phase wurde durch Preproduction und Production ersetzt. Zudem wurde das Produkt auch für eine weitere Distribution (CentOS) unterstützt. Fünf leistungsstarke physische Server und ein Dutzend virtuelle Server kamen hinzu. Entwicklern und Testern wurde es zunehmend schwieriger, ihre Aufgaben in einer Umgebung zu testen, die dem Produktionszustand einigermaßen nahekam. Zu diesem Zeitpunkt wurde klar, dass man nicht ohne sie auskam...
Teil II

Unser Cluster stellt ein beeindruckendes System aus mehreren Dutzend separaten Komponenten dar, die nicht durch Dockerfiles beschrieben werden. Es kann nur insgesamt für das bestimmte Deploy-Umfeld konfiguriert werden. Unsere Aufgabe besteht darin, den Cluster in der Staging-Umgebung zu deployen, um ihn vor den Pre-Release-Tests auszuprobieren.
Theoretisch können mehrere Cluster gleichzeitig betrieben werden: so viele wie Aufgaben im abgeschlossenen oder nahezu abgeschlossenen Zustand. Die Ressourcen der uns zur Verfügung stehenden Server ermöglichen das Starten mehrerer Cluster auf jedem Server. Jeder Staging-Cluster muss isoliert sein (es darf keine Überlappungen bei Ports, Verzeichnissen usw. geben).
Die wertvollste Ressource ist unsere Zeit, und davon hatten wir nicht viel.
Für einen schnelleren Start haben wir Docker Swarm aufgrund seiner Einfachheit und Flexibilität der Architektur gewählt. Das erste, was wir gemacht haben, war, einen Manager und mehrere Nodes auf den Remote-Servern zu erstellen:
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
kilqc94pi2upzvabttikrfr5d nop-test-1 Bereit Aktiv 19.03.2
jilwe56pl2zvabupryuosdj78 nop-test-2 Bereit Aktiv 19.03.2
j5a4yz1kr2xke6b1ohoqlnbq5 * nop-test-3 Bereit Aktiv Leiter 19.03.2
Dann haben wir ein Netzwerk erstellt:
$ docker network create --driver overlay --subnet 10.10.10.0/24 nw_swarm
Anschließend haben wir Gitlab-CI und die Swarm-Nodes in Bezug auf die Remote-Verwaltung der Nodes aus der CI verbunden: Installation von Zertifikaten, Konfiguration von geheimen Variablen sowie die Einrichtung des Docker-Dienstes auf dem verwaltenden Server. Dies hat uns viel Zeit gespart.
Dann haben wir Jobs zur Erstellung und Zerstörung des Stacks in der .gitlab-ci.yml hinzugefügt.
In der .gitlab-ci.yml wurden einige zusätzliche Jobs hinzugefügt.
## 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
Aus dem obigen Code-Ausschnitt ist ersichtlich, dass in den Pipelines zwei Schaltflächen (deploy_staging, stop_staging) hinzugefügt wurden, die manuelles Eingreifen erfordern.

Der Name des Stacks entspricht dem Namen des Branches, und diese Einzigartigkeit sollte ausreichend sein. Die Dienste im Stack erhalten einzigartige IP-Adressen, während Ports, Verzeichnisse usw. isoliert, aber von Stack zu Stack identisch sind (da die Konfigurationsdatei für alle Stacks gleich ist) – genau das haben wir angestrebt. Den Stack (Cluster) richten wir mit Hilfe von docker-compose.yml, in dem unser Cluster beschrieben ist.
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
Hier ist zu erkennen, dass die Komponenten durch ein Netzwerk (nw_swarm) verbunden und untereinander zugänglich sind.
Die systematischen Komponenten (basierend auf Redis, MySQL) sind vom gemeinsamen Pool der benutzerdefinierten Komponenten getrennt (in Planung ist auch, diese als separate Dienste zu splitten). Die Deploy-Phase unseres Clusters sieht wie der Transfer von CMD in unser großes, konfiguriertes Image aus und unterscheidet sich im Grunde kaum von dem in Teil I beschriebenen Deployment. Ich möchte die Unterschiede betonen:
- git clone … — wir erhalten die notwendigen Dateien für das Deployment (createconfig.py, install_venv.sh usw.)
- curl… && unzip … — wir laden die Build-Artefakte herunter und entpacken sie (kompilierte Tools)
Es bleibt nur noch ein bisher nicht beschriebenes Problem: Die Komponenten mit Web-Interface sind nicht aus den Browsern der Entwickler zugänglich. Wir lösen dieses Problem mit einem Reverse Proxy wie folgt:
In der .gitlab-ci.yml fügen wir nach dem Deployment des Cluster-Stacks eine Zeile zum Deployment des Load Balancers hinzu (der bei Commits nur seine Konfiguration aktualisiert (neue nginx-Konfigurationsdateien nach dem Muster: /etc/nginx/conf.d/${CI_COMMIT_REF_NAME}.conf erstellt) — siehe 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
Auf den Entwicklercomputern aktualisieren wir /etc/hosts; wir schreiben die URL für nginx hinein:
10.50.173.106 staging_BRANCH-1831_cluster.dev
So, das Deployment isolierter Staging-Cluster ist umgesetzt, und die Entwickler können diese nun in ausreichender Anzahl für ihre Aufgaben überprüfen.
Weitere Pläne:
- Unsere Komponenten als Services aufteilen
- Für jedes Dockerfile eine separate Datei anlegen
- Automatisch weniger ausgelastete Knoten im Stack bestimmen
- Knoten nach Namensmuster festlegen (statt ID wie im Artikel zu verwenden)
- Überprüfung hinzufügen, dass der Stack gelöscht wurde
- …
Besonderer Dank für .
Quelle: habr.com
