Il sistema di raccomandazione video online su cui stiamo lavorando è un progetto commerciale privato e tecnicamente consiste in un cluster multi-componente composto da elementi proprietari e open source. L'obiettivo di questo articolo è descrivere l'implementazione del sistema di clustering Docker Swarm per l'ambiente di staging, senza interrompere il flusso di lavoro dei nostri processi in un contesto di tempo limitato. Il racconto che vi presentiamo è diviso in due parti. La prima parte descrive CI/CD prima di utilizzare Docker Swarm, mentre la seconda passa al processo di implementazione. Chi non fosse interessato a leggere la prima parte può passare liberamente alla seconda.
Parte I
In un lontano anno, era necessario configurare il processo CI/CD il più rapidamente possibile. Una delle condizioni era di non utilizzare Docker per il deployment dei componenti sviluppati per vari motivi:
- per un funzionamento più affidabile e stabile dei componenti in produzione (ossia richiedeva praticamente di non utilizzare la virtualizzazione)
- i principali sviluppatori non volevano lavorare con Docker (strano, ma era proprio così)
- per motivi ideologici del management R&D
L'infrastruttura, il stack e i requisiti di base per un MVP erano i seguenti:
- 4 server Intel® X5650 con Debian (una macchina più potente completamente dedicata allo sviluppo)
- Sviluppo di componenti personalizzati realizzato in C++, Python3
- Principali strumenti di terze parti utilizzati: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
- Pipeline per la costruzione e il testing dei componenti separate per debug e release
Una delle prime questioni da risolvere in fase iniziale è come verranno distribuiti i componenti personalizzati in un determinato ambiente (CI/CD).
I componenti esterni sono stati installati e aggiornati a livello di sistema. Le applicazioni personalizzate, sviluppate in C++ o Python, possono essere distribuite in vari modi. Tra questi, ad esempio: creazione di pacchetti di sistema, invio al repository delle immagini create e successiva installazione sui server. Per motivi sconosciuti è stato scelto un altro approccio, ovvero: tramite CI vengono compilati gli eseguibili delle applicazioni, viene creato un ambiente virtuale di progetto, vengono installati i moduli py da requirements.txt e tutti questi artefatti, insieme a configurazioni, script e ambienti associati, vengono inviati sui server. Successivamente, le applicazioni vengono avviate da un utente virtuale senza diritti di amministratore.
Come sistema CI/CD è stato scelto Gitlab-CI. Il pipeline risultante appariva circa così:

Strutturalmente gitlab-ci.yml appariva nel seguente modo
---
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
## fase di testing
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
## fase di staging
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 test vengono eseguiti su un'immagine personalizzata, in cui sono già installati tutti i pacchetti di sistema necessari e sono state effettuate altre configurazioni.
Sebbene ognuno di questi script nei job sia interessante a suo modo, non entrerò nei dettagli di ciascuno, poiché richiederebbe molto tempo e non è questo l'obiettivo dell'articolo. Vorrei solo sottolineare che la fase di deploy consiste in una sequenza di chiamate agli script:
- createconfig.py — crea il file settings.ini con le configurazioni dei componenti in vari ambienti per il successivo deploy (Preproduttivo, Produttivo, Testing, …)
- install_venv.sh — crea un ambiente virtuale per i componenti Python in una directory specifica 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
Col tempo, le fasi di staging sono state sostituite da preproduzione e produzione. È stata aggiunta la supporto del prodotto su un'altra distribuzione (CentOS). Sono stati aggiunti altri 5 potenti server fisici e una decina di server virtuali. Gli sviluppatori e i tester avevano sempre più difficoltà a testare le loro attività in un ambiente che fosse almeno paragonabile a quello di produzione. A questo punto è diventato chiaro che non si poteva fare a meno di esso…
Parte II

Quindi, il nostro cluster si presenta come un sistema piuttosto impressionante composto da un paio di decine di componenti separati, non descritti dai Dockerfile. Configurarlo per il deployment in un determinato ambiente è possibile solo in linea generale. Il nostro obiettivo è effettuare il deployment del cluster nell'ambiente di staging per collaudarlo prima dei test di pre-lancio.
Teoricamente, possono esserci diversi cluster operativi contemporaneamente: tanto quanto le attività in stato completato o quasi completo. Le risorse disponibili sui server ci permettono di eseguire diversi cluster su ciascun server. Ogni cluster di staging deve essere isolato (non devono esserci sovrapposizioni su porte, directory, ecc.).
La risorsa più preziosa è il nostro tempo, e ne avevamo poco.
Per un avvio più rapido, abbiamo scelto Docker Swarm per la sua semplicità e flessibilità architettonica. La prima cosa che abbiamo fatto è stata creare un manager e diverse 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 una rete:
$ docker network create --driver overlay --subnet 10.10.10.0/24 nw_swarm
Dopo, abbiamo collegato Gitlab-CI e le nodi di Swarm per la gestione remota delle nodi tramite CI: installazione dei certificati, configurazione delle variabili segrete, e configurazione del servizio Docker sul server di gestione. Questo ci ha fatto risparmiare molto tempo.
Dopo, abbiamo aggiunto job per la creazione e distruzione dello stack in .gitlab-ci .yml.
In .gitlab-ci .yml sono state aggiunte anche alcune 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
Dall'estratto di codice sopra, si può vedere che nei Pipelines sono state aggiunte due bottoni (deploy_staging, stop_staging), che richiedono un intervento 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 porte, directory, ecc. saranno isolate ma identiche da stack a stack (poiché il file di configurazione è lo stesso per tutti gli stack) — proprio ciò che volevamo. Lo stack (cluster) lo realizziamo 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 può vedere che i componenti sono uniti 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 (in futuro anche i componenti personalizzati saranno separati come servizi). La fase di deployment del nostro cluster si presenta come il passaggio di CMD nel nostro grande image configurato e in generale non si differenzia praticamente da un deployment descritto nella Parte I. Sottolineo le differenze:
- git clone … — otteniamo i file necessari per eseguire il deployment (createconfig.py, install_venv.sh, ecc.)
- curl… && unzip … — scarichiamo e decomprimiamo gli artefatti di build (utilità compilate)
Resta solo un problema non ancora descritto: i componenti che hanno un'interfaccia web non sono accessibili dai browser degli sviluppatori. Risolviamo questo problema utilizzando un reverse proxy in questo modo:
In .gitlab-ci.yml, dopo il deployment dello stack del cluster, aggiungiamo la riga per il deployment del bilanciatore (che, durante i 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 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
Aggiorniamo /etc/hosts sui computer degli sviluppatori; impostiamo l'URL fino a nginx:
10.50.173.106 staging_BRANCH-1831_cluster.dev
Quindi, il deployment di cluster 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 ciascuno
- Identificare automaticamente i nodi meno sovraccarichi nello stack
- Impostare i nodi in base al modello del nome (anziché utilizzare l'ID come nell'articolo)
- Aggiungere un controllo che verifichi che lo stack sia distrutto
- …
Un ringraziamento speciale a .
Fonte: habr.com
