Il sistema di raccomandazione online per contenuti video su cui stiamo lavorando è uno sviluppo commerciale chiuso e tecnicamente consiste in un cluster multi-componente di componenti proprietari e open source. L'obiettivo di questo articolo è descrivere l'implementazione del sistema di clustering docker swarm per l'ambiente di staging, senza compromettere il workflow consolidato dei nostri processi in condizioni di tempo limitato. La narrazione presentata alla vostra attenzione è divisa in due parti. La prima parte descrive CI/CD prima dell'uso di docker swarm, mentre la seconda tratta il suo processo di implementazione. Chi non è interessato a leggere la prima parte può passare direttamente alla seconda.
Parte I
In un anno lontano, era necessario impostare il processo CI/CD il più rapidamente possibile. Una delle condizioni era di non utilizzare Docker per il deployment dei componenti in fase di sviluppo per diversi motivi:
- per un funzionamento più affidabile e stabile dei componenti in produzione (cioè, in sostanza, il requisito di non utilizzare la virtualizzazione)
- i principali sviluppatori non volevano lavorare con Docker (strano, ma era così)
- per ragioni ideologiche della direzione R&D
L'infrastruttura, il stack e i requisiti iniziali indicativi per l'MVP erano i seguenti:
- 4 server Intel® X5650 con Debian (una macchina più potente completamente dedicata allo sviluppo)
- Lo sviluppo di componenti personalizzati avviene in C++, Python3
- Principali strumenti di terze parti utilizzati: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
- Pipeline di costruzione e testing dei componenti separatamente per debug e release
Una delle prime domande da risolvere nella fase iniziale è come avverrà il deployment dei componenti personalizzati in un ambiente (CI/CD).
I componenti esterni sono stati decisi di essere installati e aggiornati sistematicamente. Le applicazioni personalizzate, sviluppate in C++ o Python, possono essere distribuite in diversi modi. Tra questi, ad esempio: la creazione di pacchetti di sistema, l'invio nel repository delle immagini raccolte e il loro successivo installazione sui server. Per ragioni sconosciute è stata scelta un'altra modalità, ovvero: tramite CI vengono compilati i file eseguibili delle applicazioni, viene creato un ambiente virtuale di progetto, vengono installati i moduli Py dai requirements.txt e tutti questi artefatti vengono inviati insieme ai file di configurazione, script e ambiente accessorio delle applicazioni sui server. Successivamente vengono avviate le applicazioni da un utente virtuale senza diritti di amministratore.
È stato scelto Gitlab-CI come sistema CI/CD. Il pipeline risultante appariva circa così:

Strutturalmente il file gitlab-ci.yml appariva come segue
---
variables:
# versione minima della CPU sui server dove viene distribuito il cluster
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
È importante notare che la costruzione e il testing vengono effettuati su un'immagine propria, dove sono già installati tutti i pacchetti di sistema necessari e sono state eseguite altre configurazioni.
Sebbene ciascuno di questi script nelle job sia interessante a modo suo, non parlerò ovviamente di ciascuno di essi, poiché la descrizione di ognuno richiederebbe tempo significativo e non è questo l'obiettivo dell'articolo. Vorrei solo sottolineare che la fase di deployment consiste in una sequenza di chiamate agli script:
- createconfig.py — crea il file settings.ini con le impostazioni dei componenti in vari ambienti per il successivo deployment (Preproduzione, Produzione, Testing, ...)
- install_venv.sh — crea un ambiente virtuale per i componenti py in una specifica directory e lo copia sui server remoti
- prepare_init.d.py — prepara gli script di avvio e arresto dei componenti basandosi su un modello
- deploy.py — distribuisce e riavvia i nuovi componenti
Il tempo passava. La fase di staging è stata sostituita dalla preproduzione e dalla produzione. È stata aggiunta la supporto per il prodotto su un'altra distribuzione (CentOS). Sono stati aggiunti altri 5 potenti server fisici e una dozzina di virtuali. E per gli sviluppatori e i tester diventava sempre più difficile testare i propri compiti in un ambiente più o meno vicino a uno stato operativo. In quel momento è diventato chiaro che non si poteva fare a meno di esso...
Parte II

Quindi, il nostro cluster è ancora uno spettacolo, un sistema di una ventina di componenti separati, non descritti da Dockerfile. Configurarlo per il deployment in un ambiente specifico è possibile solo in generale. Il nostro compito è deployare il cluster in un ambiente di staging per testarlo prima del collaudo finale.
Teoricamente, possono esserci più cluster in funzione contemporaneamente: tanti quanti sono i task in stato completato o vicino al completamento. Le risorse disponibili sui server a nostra disposizione permettono di eseguire più cluster su ogni server. Ogni cluster di staging deve essere isolato (non devono esserci sovrapposizioni di porte, directory, ecc.).
La risorsa più preziosa è il nostro tempo, e ne avevamo poco.
Per una rapida avvio, abbiamo scelto Docker Swarm per la sua semplicità e flessibilità dell'architettura. La prima cosa che abbiamo fatto è stata creare un manager e alcuni nodi sui server remoti:
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
kilqc94pi2upzvabttikrfr5d nop-test-1 Pronto Attivo 19.03.2
jilwe56pl2zvabupryuosdj78 nop-test-2 Pronto Attivo 19.03.2
j5a4yz1kr2xke6b1ohoqlnbq5 * nop-test-3 Pronto Attivo Leader 19.03.2
Successivamente, abbiamo creato la rete:
$ docker network create --driver overlay --subnet 10.10.10.0/24 nw_swarm
Successivamente, abbiamo collegato Gitlab-CI e i nodi Swarm per la gestione remota dei nodi dal CI: installazione dei certificati, configurazione delle variabili segrete e configurazione del servizio Docker sul server di gestione. Questo ci ha fatto risparmiare molto tempo.
Successivamente, abbiamo aggiunto i job per la creazione e distruzione dello stack in .gitlab-ci.yml.
In .gitlab-ci.yml sono stati aggiunti altri 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
Dal frammento di codice sopra, si vede che nei Pipelines sono state aggiunte due pulsanti (deploy_staging, stop_staging) che richiedono un'interazione manuale.

Il nome dello stack corrisponde al nome del ramo e questa unicità dovrebbe essere sufficiente. I servizi nello stack ricevono indirizzi IP unici, mentre le porte, le directory, ecc. saranno isolate, ma identiche da uno stack all'altro (poiché il file di configurazione è identico per tutti gli stack) — questo è ciò che volevamo ottenere. Lo stack (cluster) viene implementato utilizzando docker-compose.yml, in cui è descritto il nostro 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
Qui si vede che i componenti sono connessi da una rete (nw_swarm) e sono accessibili l'uno all'altro.
I componenti di sistema (basati su redis, mysql) sono separati dal pool generale di componenti personalizzati (dove sono previsti anche i componenti personalizzati divisi come servizi). La fase di deploy del nostro cluster appare come la trasmissione del CMD nella nostra unica grande immagine configurata e in generale non differisce praticamente dal deploy descritto nella Parte I. Sottolineo le differenze:
- git clone … — otteniamo i file necessari per effettuare il deploy (createconfig.py, install_venv.sh, ecc.)
- curl… && unzip … — scarichiamo e decomprimiamo gli artefatti di build (utilità compilati)
Rimane solo un problema finora non descritto: i componenti che hanno un'interfaccia web non sono accessibili dai browser degli sviluppatori. Stiamo risolvendo questo problema utilizzando un reverse proxy, in questo modo:
Nel .gitlab-ci.yml, dopo il deployment dello stack del cluster, aggiungiamo la riga per il deployment del bilanciatore (che, in caso di commit, aggiorna solo la sua configurazione (crea nuovi file di configurazione nginx secondo il modello: /etc/nginx/conf.d/${CI_COMMIT_REF_NAME}.conf) — vedi il codice 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
Sui computer degli sviluppatori aggiorniamo /etc/hosts; scriviamo l'url fino a nginx:
10.50.173.106 staging_BRANCH-1831_cluster.dev
Quindi, il deployment di cluster di staging isolati è stato realizzato e gli sviluppatori possono ora avviarli in un numero sufficiente per verificare le loro attività.
Piani futuri:
- Separare i nostri componenti come servizi
- Creare un Dockerfile per ognuno
- Determinare automaticamente i nodi meno sovraccarichi nello stack
- Specificare i nodi secondo un modello di nome (e non utilizzare l'id come nell'articolo)
- Aggiungere un controllo per verificare che lo stack sia stato distrutto
- …
Un ringraziamento speciale per .
Fonte: habr.com
