System rekomendacji treści wideo online, nad którym pracujemy, jest zamkniętym rozwojem komercyjnym i technicznie jest wieloskładnikowym klastrem komponentów zastrzeżonych i open source. Celem napisania tego artykułu jest opisanie implementacji systemu klastrowania roju dokerów dla miejsca pomostowego bez zakłócania ustalonego przepływu pracy naszych procesów w ograniczonym czasie. Przedstawiona wam narracja jest podzielona na dwie części. Pierwsza część opisuje CI/CD przed użyciem docker swarm, a druga opisuje proces jej implementacji. Osoby, które nie są zainteresowane przeczytaniem pierwszej części, mogą spokojnie przejść do drugiej.
Część I.
W odległym, odległym roku konieczne było jak najszybsze ustawienie procesu CI/CD. Jednym z warunków było nieużywanie Dockera do rozmieszczenia opracowane komponenty z kilku powodów:
- dla bardziej niezawodnego i stabilnego działania komponentów w Produkcji (czyli w zasadzie wymóg niekorzystania z wirtualizacji)
- wiodący programiści nie chcieli pracować z Dockerem (dziwne, ale tak było)
- zgodnie z ideologicznymi uwarunkowaniami kierownictwa B+R
Infrastruktura, stos i przybliżone wymagania początkowe dla MVP zostały przedstawione w następujący sposób:
- 4 serwery Intel® X5650 z Debianem (jeden bardziej wydajny komputer jest w pełni opracowany)
- Rozwój własnych, niestandardowych komponentów odbywa się w językach C++, Python3
- Główne używane narzędzia zewnętrzne: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
- Potoki do budowania i testowania komponentów oddzielnie w celu debugowania i wydawania
Jednym z pierwszych pytań, na które należy odpowiedzieć na początkowym etapie, jest to, w jaki sposób niestandardowe komponenty zostaną wdrożone w dowolnym środowisku (CI / CD).
Postanowiliśmy systemowo instalować komponenty innych firm i systemowo je aktualizować. Niestandardowe aplikacje opracowane w C++ lub Pythonie można wdrażać na kilka sposobów. Wśród nich na przykład: tworzenie pakietów systemowych, wysyłanie ich do repozytorium zbudowanych obrazów, a następnie instalowanie ich na serwerach. Z nieznanego powodu wybrano inną metodę, a mianowicie: za pomocą CI kompilowane są pliki wykonywalne aplikacji, tworzone jest wirtualne środowisko projektowe, instalowane są moduły py z requirements.txt, a wszystkie te artefakty są przesyłane wraz z konfiguracjami, skryptami i środowiska aplikacyjnego towarzyszącego serwerom. Następnie aplikacje są uruchamiane jako użytkownik wirtualny bez uprawnień administratora.
Jako system CI/CD wybrano Gitlab-CI. Wynikowy potok wyglądał mniej więcej tak:
Strukturalnie gitlab-ci.yml wyglądał tak
---
variables:
# минимальная версия ЦПУ на серверах, где разворачивается кластер
CMAKE_CPUTYPE: "westmere"
DEBIAN: "MYREGISTRY:5000/debian:latest"
before_script:
- eval $(ssh-agent -s)
- ssh-add <(echo "$SSH_PRIVATE_KEY")
- mkdir -p ~/.ssh && echo -e "Host *ntStrictHostKeyChecking nonn" > ~/.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
Warto zauważyć, że montaż i testowanie odbywa się na własnym obrazie, na którym zostały już zainstalowane wszystkie niezbędne pakiety systemowe i dokonano innych ustawień.
Chociaż każdy z tych skryptów w pracy jest ciekawy na swój sposób, ale oczywiście nie będę o nich mówić.Opis każdego z nich zajmie dużo czasu i nie to jest celem artykułu. Zwrócę tylko uwagę na fakt, że etap wdrożenia składa się z sekwencji wywołań skryptów:
- utwórzkonfigurację.py - tworzy plik settings.ini z ustawieniami komponentów w różnych środowiskach do późniejszego wdrożenia (preprodukcja, produkcja, testowanie, ...)
- install_venv.sh - tworzy wirtualne środowisko dla komponentów py w określonym katalogu i kopiuje je na zdalne serwery
- przygotowanie_init.d.py — przygotowuje skrypty start-stop dla komponentu na podstawie szablonu
- wdrożyć.py - rozkłada i ponownie uruchamia nowe komponenty
Czas minął. Etap inscenizacji został zastąpiony przez preprodukcję i produkcję. Dodano obsługę produktu w jeszcze jednej dystrybucji (CentOS). Dodano 5 potężniejszych serwerów fizycznych i tuzin wirtualnych. Deweloperom i testerom coraz trudniej było testować swoje zadania w środowisku mniej więcej zbliżonym do stanu roboczego. W tym czasie stało się jasne, że nie można się bez niego obejść ...
część druga
Nasz klaster jest więc spektakularnym systemem kilkudziesięciu oddzielnych komponentów, których nie opisują Dockerfile. Można go ogólnie skonfigurować tylko do wdrożenia w określonym środowisku. Naszym zadaniem jest wdrożenie klastra w środowisko pomostowe w celu przetestowania go przed testami przedpremierowymi.
Teoretycznie jednocześnie może działać kilka klastrów: tyle, ile jest zadań w stanie zakończonym lub bliskim ukończenia. Możliwości serwerów, którymi dysponujemy, pozwalają na uruchomienie kilku klastrów na każdym serwerze. Każdy klaster pomostowy musi być izolowany (porty, katalogi itp. nie mogą się przecinać).
Naszym najcenniejszym zasobem jest nasz czas, a nie mieliśmy go zbyt wiele.
Na szybszy start wybraliśmy Docker Swarm ze względu na jego prostotę i elastyczność architektury. Pierwszą rzeczą, którą zrobiliśmy, było utworzenie menedżera i kilku węzłów na zdalnych serwerach:
$ 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
Następnie utwórz sieć:
$ docker network create --driver overlay --subnet 10.10.10.0/24 nw_swarm
Następnie połączyliśmy węzły Gitlab-CI i Swarm pod kątem zdalnego sterowania węzłami z CI: instalowania certyfikatów, ustawiania tajnych zmiennych i konfigurowania usługi Docker na serwerze sterującym. Ten
Następnie dodaliśmy zadania tworzenia i niszczenia stosu do .gitlab-ci .yml.
Do .gitlab-ci .yml dodano kilka dodatkowych zadań
## 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
Z powyższego fragmentu kodu widać, że dwa przyciski (deploy_staging, stop_staging) zostały dodane do potoków, co wymaga ręcznego działania.
Nazwa stosu jest zgodna z nazwą gałęzi i ta unikalność powinna wystarczyć. Usługi w stosie otrzymują unikalne adresy IP, a porty, katalogi itp. będzie izolowany, ale taki sam od stosu do stosu (bo plik konfiguracyjny jest taki sam dla wszystkich stosów) - o co nam chodziło. Wdrażamy stos (klaster) za pomocą docker-compose.yml, który opisuje nasz klaster.
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
Tutaj widać, że komponenty są połączone jedną siecią (nw_swarm) i są dla siebie dostępne.
Komponenty systemu (oparte na redis, mysql) są odseparowane od ogólnej puli komponentów niestandardowych (w planach i niestandardowe są podzielone jako usługi). Etap wdrożenia naszego klastra wygląda jak przekazanie CMD do naszego jednego dużego skonfigurowanego obrazu i generalnie praktycznie nie różni się od wdrożenia opisanego w części I. Podkreślę różnice:
- klon gita... - pobierz pliki potrzebne do wdrożenia (createconfig.py, install_venv.sh itp.)
- zwiń... && rozpakuj... - pobieranie i rozpakowywanie artefaktów kompilacji (skompilowane narzędzia)
Jest tylko jeden jeszcze nieopisany problem: komponenty posiadające interfejs WWW nie są dostępne z przeglądarek programistów. Rozwiązujemy ten problem za pomocą odwrotnego proxy, w ten sposób:
W .gitlab-ci.yml po wdrożeniu stosu klastrów dodajemy linijkę wdrażania balancera (który przy zatwierdzeniu aktualizuje tylko swoją konfigurację (tworzy nowe pliki konfiguracyjne nginx według szablonu: /etc/nginx/conf. d/${CI_COMMIT_REF_NAME}.conf) — zobacz kod 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
Na komputerach deweloperskich zaktualizuj plik /etc/hosts; przepisać adres URL do nginx:
10.50.173.106 staging_BRANCH-1831_cluster.dev
Wdrożono więc wdrożenie izolowanych klastrów pomostowych, a programiści mogą teraz uruchamiać je w dowolnej liczbie wystarczającej do sprawdzenia ich zadań.
Przyszłe plany:
- Oddziel nasze komponenty jako usługi
- Mieć dla każdego pliku Dockerfile
- Automatycznie wykrywaj mniej obciążone węzły w stosie
- Określ węzły według wzorca nazw (zamiast używania identyfikatora, jak w artykule)
- Dodaj sprawdzenie, czy stos jest zniszczony
- ...
Specjalne podziękowania za
Źródło: www.habr.com