Sistemul online de recomandare a conținutului video pe care lucrăm este un proiect comercial închis și reprezintă din punct de vedere tehnic un cluster multi-componente format din componente proprii și open source. Scopul acestui articol este de a descrie implementarea sistemului de clusterizare docker swarm pentru o platformă de staging, fără a afecta fluxul de lucru stabilit al proceselor noastre în condiții de timp limitat. Narațiunea prezentată este împărțită în două părți. Prima parte descrie CI/CD-ul înainte de utilizarea docker swarm, iar a doua — procesul implementării acestuia. Cei care nu sunt interesați de citirea primei părți pot trece direct la a doua.
Partea I
Într-un an îndepărtat, era necesar să configurăm cât mai repede procesul CI/CD. Una dintre condiții era să nu folosim Docker pentru desfășurarea componentelor dezvoltate din mai multe motive:
- pentru o funcționare mai fiabilă și stabilă a componentelor în Production (adică, în esență, cerință de a nu utiliza virtualizarea)
- dezvoltatorii de frunte nu doreau să lucreze cu Docker (ciudat, dar așa era)
- din considerații ideologice ale conducerii R&D
Infrastructura, stiva și cerințele aproximative pentru MVP erau următoarele:
- 4 servere Intel® X5650 cu Debian (un server mai puternic dedicat complet dezvoltării)
- Dezvoltarea componentelor personalizate se face în C++, Python3
- Principalele instrumente third-party utilizate: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
- Pipelines de construire și testare a componentelor separat pentru debug și release
Una dintre primele întrebări care trebuie rezolvate în stadiul inițial este cum va fi realizată desfășurarea componentelor personalizate în orice mediu (CI/CD).
Componente externe au decis să fie instalate și actualizate sistematic. Aplicațiile personalizate, dezvoltate în C++ sau Python, pot fi desfășurate în mai multe moduri. Printre acestea se numără, de exemplu: crearea de pachete sistemice, trimiterea acestora în repository-ul imaginilor compilate și instalarea lor ulterioară pe servere. Dintr-un motiv necunoscut, a fost ales o altă abordare, și anume: cu ajutorul CI se compilează fișiere executabile ale aplicațiilor, se creează un mediu virtual de proiect, se instalează modulele py din requirements.txt și toate aceste artefacte sunt trimise împreună cu configurațiile, scripturile și mediul asociat aplicațiilor pe servere. Apoi, aplicațiile sunt lansate de un utilizator virtual fără drepturi de administrator.
Sistemul CI/CD ales a fost Gitlab-CI. Pipeline-ul rezultat arăta cam așa:

Structura gitlab-ci.yml arăta astfel
---
variables:
# versiunea minimă a CPU-ului pe serverele unde se desfășoară cluster-ul
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
Este de remarcat că build-ul și testarea se desfășoară pe propria imagine, în care sunt deja instalate toate pachetele sistemice necesare și au fost efectuate alte configurări.
Deși fiecare dintre aceste scripturi în job-uri este interesant în felul său, nu voi vorbi desigur despre ele; descrierea fiecăruia ar dura semnificativ și nu aceasta este scopul articolului. Voi sublinia doar că etapa de deployment constă dintr-o succesiune de apeluri de scripturi:
- createconfig.py — creează fișierul settings.ini cu setările componentelor în diferite medii pentru următoarea desfășurare (Preproduction, Production, Testing, …)
- install_venv.sh — creează un mediu virtual pentru componentele py într-un director specific și îl copiază pe serverele de la distanță
- prepare_init.d.py — pregătește scripturile de start/dop pentru componente pe baza unui șablon
- deploy.py — desfășoară și repornește componentele noi
A trecut ceva timp. Etapa staging a fost înlocuită cu preproduction și production. S-a adăugat suport pentru produsul pe încă o distribuție (CentOS). Au fost adăugate încă 5 servere fizice puternice și o duzină de virtuale. Iar dezvoltatorilor și testorilor le devenea din ce în ce mai greu să își testeze sarcinile într-un mediu care să fie oarecum apropiat de cel de lucru. În acel moment a devenit clar că nu putem trăi fără el...
Partea II

Așadar, cluster-ul nostru reprezintă o adevărată vedere spectaculoasă, un sistem format din câteva zeci de componente separate, neaduse în ordine de Dockerfile-uri. Configurarea acestuia pentru desfășurarea într-un mediu specific se poate face doar în ansamblu. Sarcina noastră este să desfășurăm cluster-ul în mediul staging pentru a-l testa înainte de testarea prelanțării.
Teoretic, pot exista mai multe clustere care funcționează simultan: atât cât sunt sarcini în stare completă sau aproape de finalizare. Resursele disponibile pe serverele noastre permit desfășurarea mai multor clustere pe fiecare server. Fiecare cluster staging trebuie să fie izolat (nu trebuie să existe intersecție pe porturi, directoare etc.).
Cel mai valoros resursă este timpul nostru și nu am avut mult.
Pentru un start mai rapid, am ales Docker Swarm datorită simplității și flexibilității sale arhitecturale. Primul lucru pe care l-am făcut a fost să creăm manager și câteva noduri pe serverele la distanță:
$ 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
Apoi, am creat o rețea:
$ docker network create --driver overlay --subnet 10.10.10.0/24 nw_swarm
Apoi, am legat Gitlab-CI și nodurile Swarm în ceea ce privește administrația la distanță a nodurilor din CI: instalarea certificatelor, configurarea variabilelor secrete, precum și configurarea serviciului Docker pe serverul de management. Aceasta ne-a economisit mult timp.
Apoi, am adăugat job-uri pentru crearea și distrugerea stivei în .gitlab-ci .yml.
În .gitlab-ci .yml s-au adăugat încă câteva joburi
## 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
Din fragmentul de cod de mai sus, se observă că în Pipelines s-au adăugat două butoane (deploy_staging, stop_staging), care necesită intervenție manuală.

Numele stivei corespunde numelui ramurii și această unică identificare ar trebui să fie suficientă. Serviciile din stivă primesc adrese IP unice, iar porturile, directoarele etc. vor fi izolate, dar identice de la stivă la stivă (deoarece fișierul de configurare este același pentru toate stivele) — lucru pe care l-am urmărit. Stiva (clusterul) o desfășurăm cu ajutorul docker-compose.yml, în care este descris clusterul nostru.
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
Se observă că componentele sunt conectate la aceeași rețea (nw_swarm) și sunt accesibile una alteia.
Componentele sistemului (bazate pe redis, mysql) sunt separate de pool-ul general de componente personalizate (în planuri, și personalizatele vor fi împărțite ca servicii). Etapa de deploy a clusterului nostru arată ca transmiterea CMD într-o singură imagine mare configurată și, în general, nu se deosebește prea mult de deploy-ul descris în Partea I. Voi sublinia diferențele:
- git clone … — obținem fișierele necesare pentru a efectua deploy-ul (createconfig.py, install_venv.sh etc.)
- curl… && unzip … — descărcăm și dezarhivăm artefactele compilării (utilitarele compilate)
A rămas doar o problemă nedezvoltată: componentele care au interfață web nu sunt accesibile din browserele dezvoltatorilor. Rezolvăm această problemă cu ajutorul reverse proxy-ului, astfel:
În .gitlab-ci.yml, după desfășurarea stivei clusterei, adăugăm linia de desfășurare a echilibrorului de sarcină (care, la commit-uri, actualizează doar configurația sa (creează fișierele de configurare nginx noi după șablon: /etc/nginx/conf.d/${CI_COMMIT_REF_NAME}.conf) — vezi codul 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
Pe computerele dezvoltatorilor, actualizăm /etc/hosts; notăm URL-ul până la nginx:
10.50.173.106 staging_BRANCH-1831_cluster.dev
Așadar, desfășurarea clustelor staging izolate a fost realizată, iar dezvoltatorii pot acum să le ruleze în numărul necesar pentru a-și verifica sarcinile.
Planuri viitoare:
- Să împărțim componentele noastre ca servicii
- Să creăm un Dockerfile pentru fiecare
- Să determinăm automat nodurile cu o încărcare mai mică din stivă
- Să definim nodurile pe baza șablonului numelui (și nu să folosim ID-uri ca în articol)
- Să adăugăm o verificare pentru a garantat că stiva este distrusă
- …
Mulțumiri speciale pentru .
Sursa: habr.com
