Das Online-Empfehlungssystem für Video-Content, an dem wir arbeiten, ist eine geschlossene kommerzielle Entwicklung und stellt technisch einen mehrkomponentigen Cluster aus eigenen und Open-Source-Komponenten dar. Ziel dieses Artikels ist es, die Implementierung eines Docker Swarm-Clustering-Systems für eine Staging-Plattform zu beschreiben, ohne den bestehenden Workflow unserer Prozesse unter Zeitdruck zu stören. Die Ihnen präsentierte Erzählung ist in zwei Teile gegliedert. Der erste Teil beschreibt CI/CD vor der Verwendung von Docker Swarm, der zweite Teil den Implementierungsprozess. Wer an der ersten Teil nicht interessiert ist, kann direkt zum zweiten Teil übergehen.
Teil I
Im weit entfernten Jahr war es erforderlich, den CI/CD-Prozess so schnell wie möglich einzurichten. Eine der Bedingungen war, Docker nicht zu verwenden für das Deployment entwickelter Komponenten aus mehreren Gründen:
- für eine zuverlässigere und stabilere Funktion der Komponenten in der Produktion (d.h. im Grunde die Anforderung, keine Virtualisierung zu verwenden)
- die führenden Entwickler wollten nicht mit Docker arbeiten (seltsam, aber so war es)
- aus ideologischen Gründen der R&D-Leitung
Die Infrastruktur, der Stack und die ungefähren Anforderungen für das MVP sahen wie folgt aus:
- 4 Intel® X5650-Server mit Debian (eine leistungsstärkere Maschine ausschließlich für die Entwicklung)
- Die Entwicklung eigener benutzerdefinierter Komponenten erfolgt in C++, Python3
- Die wichtigsten 3rdparty-Tools: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
- Pipelines für die Erstellung und das Testen von Komponenten getrennt für Debug und Release
Eine der ersten Fragen, die in der Anfangsphase zu klären ist, ist, wie die Bereitstellung der benutzerdefinierten Komponenten in einer Umgebung (CI/CD) erfolgen wird.
Drittanbieter-Komponenten wurden systematisch installiert und aktualisiert. Kundenspezifische Anwendungen, die in C++ oder Python entwickelt werden, können jedoch auf verschiedene Weisen bereitgestellt werden. Dazu gehört beispielsweise: die Erstellung von Systempaketen, das Versenden dieser in ein Repository von erstellten Images und deren anschließende Installation auf Servern. Aus bisher unbekannten Gründen wurde ein anderer Ansatz gewählt, und zwar: mit Hilfe von CI werden ausführbare Dateien der Anwendungen kompiliert, eine virtuelle Projektumgebung erstellt, py-Module aus requirements.txt installiert und all diese Artefakte werden zusammen mit Konfigurationen, Skripten und der begleitenden Umgebung der Anwendungen auf die Server versendet. Anschließend werden die Anwendungen von einem virtuellen Benutzer ohne Administratorrechte gestartet.
Als CI/CD-System wurde Gitlab-CI ausgewählt. Der resultierende Pipeline sah ungefähr so aus:

Strukturell sah die gitlab-ci.yml folgendermaßen aus
---
variables:
# Minimale CPU-Version auf den 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
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
## Testphase
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-Phase
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
Es ist zu beachten, dass der Build und die Tests auf einem eigenen Image durchgeführt werden, auf dem bereits alle erforderlichen Systempakete installiert und weitere Konfigurationen vorgenommen wurden.
Obwohl jedes dieser Skripte in den Jobs auf seine Weise interessant ist, werde ich natürlich keine Beschreibung jedes einzelnen geben, da dies erheblich Zeit in Anspruch nehmen würde und nicht das Ziel des Artikels ist. Ich möchte nur darauf hinweisen, dass die Deploy-Phase aus einer Abfolge von Skriptaufrufen besteht:
- createconfig.py — erstellt die Datei settings.ini mit den Konfigurationen der Komponenten in verschiedenen Umgebungen für die anschließende Bereitstellung (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-Stopp-Skripte der Komponenten auf der 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. Die Unterstützung für ein weiteres Produkt auf einem weiteren Distributionstyp (CentOS) wurde hinzugefügt. Es kamen fünf leistungsstarke physische Server und ein Dutzend virtuelle Server hinzu. Für die Entwickler und Tester wurde es immer schwieriger, ihre Aufgaben in einer Umgebung zu testen, die dem Betriebszustand mehr oder weniger nahe kam. Zu diesem Zeitpunkt wurde deutlich, dass wir nicht darauf verzichten konnten...
Teil II

Unser Cluster ist ein ziemlich beeindruckendes System aus mehreren Dutzend einzelnen Komponenten, die nicht durch Dockerfiles beschrieben sind. Es kann nur als Ganzes für die Bereitstellung in eine bestimmte Umgebung konfiguriert werden. Unsere Aufgabe besteht darin, den Cluster in einer Staging-Umgebung bereitzustellen, um ihn vor dem Release-Testing zu testen.
Theoretisch kann es mehrere gleichzeitig betriebene Cluster geben: so viele, wie Aufgaben im abgeschlossenen oder nahezu abgeschlossenen Zustand vorhanden sind. Die Rechenleistung der Server, die uns zur Verfügung stehen, erlaubt es, mehrere Cluster auf jedem Server auszuführen. Jeder Staging-Cluster muss isoliert sein (es darf keine Überschneidungen 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 ausgewählt. Das Erste, was wir taten, war, auf den entfernten Servern einen Manager und mehrere Knoten 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
Als nächstes 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-Knoten für die Remote-Verwaltung der Knoten aus CI verbunden: Installation von Zertifikaten, Konfiguration von geheimen Variablen sowie Konfiguration des Docker-Dienstes auf dem verwaltenden Server. Das hat uns viel Zeit gespart.
Als nächstes haben wir Jobs für die Erstellung und Zerstörung des Stacks in .gitlab-ci.yml hinzugefügt.
In der .gitlab-ci.yml wurden noch einige 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 oben genannten Codefragment ist ersichtlich, dass in die Pipelines zwei Schaltflächen (deploy_staging, stop_staging) hinzugefügt wurden, die manuelle Eingriffe 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 in jedem Stack gleich sein werden (da die Konfigurationsdatei für alle Stacks identisch ist) - genau das, was wir erreichen wollten. Den Stack (Cluster) setzen 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 sehen, dass die Komponenten über ein Netzwerk (nw_swarm) verbunden sind und einander zugänglich sind.
Die Systemkomponenten (basierend auf Redis, MySQL) sind vom gemeinsamen Pool der benutzerdefinierten Komponenten getrennt (es ist geplant, auch die benutzerdefinierten als Dienste zu trennen). Die Bereitstellungsphase unseres Clusters sieht aus wie die Übergabe von CMD an unser großes konfiguriertes Image und unterscheidet sich insgesamt kaum von der in Teil I beschriebenen Bereitstellung. Ich möchte die Unterschiede hervorheben:
- git clone … – wir erhalten die Dateien, die für die Bereitstellung erforderlich sind (createconfig.py, install_venv.sh usw.)
- curl… && unzip … – wir laden die Build-Artefakte herunter und entpacken sie (kompilierte Utilities)
Es gibt nur noch ein bisher nicht beschriebenes Problem: Komponenten, die eine Weboberfläche haben, sind aus den Entwickler-Browsern nicht zugänglich. Wir lösen dieses Problem mit einem Reverse Proxy, und zwar so:
In der .gitlab-ci.yml fügen wir nach dem Deployment des Cluster-Stacks die Zeile für das Deployment des Load Balancers hinzu (der bei Commits nur seine Konfiguration aktualisiert (neue Konfigurationsdateien für nginx nach dem Muster: /etc/nginx/conf.d/${CI_COMMIT_REF_NAME}.conf erstellt) – siehe den 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 Entwicklermaschinen aktualisieren wir /etc/hosts; wir tragen die URL bis nginx ein:
10.50.173.106 staging_BRANCH-1831_cluster.dev
Somit ist das Deployment der isolierten Staging-Cluster umgesetzt, und die Entwickler können diese nun in der erforderlichen Menge für ihre Aufgaben testen.
Weitere Pläne:
- Unsere Komponenten als Dienste trennen
- Für jede Dockerfile einen eigenen anlegen
- Automatisch weniger ausgelastete Knoten im Stack bestimmen
- Knoten anhand des Namensmusters zuweisen (anstatt der ID wie im Artikel zu verwenden)
- Überprüfen, dass der Stack zerstört wurde
- …
Besonderer Dank für .
Quelle: habr.com
