Wdrażanie aplikacji za pomocą Docker Swarm

Online'owy system rekomendacji treści wideo, nad którym pracujemy, jest zamkniętym projektem komercyjnym i technicznie stanowi wieloskładnikowy klaster z własnych i komponentów typu open source. Celem napisania tego artykułu jest opis wdrożenia systemu klastrowania docker swarm na etapie staging, nie naruszając ustalonego workflow naszych procesów w warunkach ograniczonego czasu. Przedstawiona narracja podzielona jest na dwie części. Pierwsza część opisuje CI/CD przed użyciem docker swarm, a druga — proces jego wdrożenia. Kto nie jest zainteresowany czytaniem pierwszej części, może przejść od razu do drugiej.

Część I

W odległym roku wymagano jak najszybszego skonfigurowania procesu CI/CD. Jednym z warunków było nieużywanie Dockera do wdrożenia opracowywanych komponentów z kilku powodów:

  • dla bardziej niezawodnej i stabilnej pracy komponentów w produkcji (tj. w zasadzie wymóg nieużywania wirtualizacji)
  • główni programiści nie chcieli pracować z Dockerem (dziwne, ale tak było)
  • z powodów ideologicznych kierownictwa R&D

Infrastruktura, stos i wstępne wymagania dla MVP przedstawiały się następująco:

  • 4 serwery Intel® X5650 z Debianem (jedna bardziej wydajna maszyna całkowicie pod rozwój)
  • Rozwój własnych dostosowanych komponentów odbywa się w C++, Python3
  • Główne wykorzystywane narzędzia 3rdparty: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
  • Pipelines budowy i testowania komponentów osobno dla debug i release

Jednym z pierwszych pytań, które należy rozwiązać na wczesnym etapie, jest sposób, w jaki będą wdrażane dostosowane komponenty w jakimś środowisku (CI/CD).

Zewnętrzne komponenty postanowiono instalować i aktualizować systemowo. Aplikacje niestandardowe, rozwijane 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 repozytoriów zbudowanych obrazów i ich późniejsza instalacja na serwerach. Z nieznanych już powodów wybrano inny sposób, a mianowicie: za pomocą CI kompilowane są pliki wykonywalne aplikacji, tworzona jest wirtualna przestrzeń robocza projektu, instalowane są moduły py z pliku requirements.txt, a wszystkie te artefakty są wysyłane razem z konfiguracjami, skryptami i towarzyszącym środowiskiem aplikacji na serwery. Następnie uruchamiane są aplikacje z konta wirtualnego użytkownika bez uprawnień administratora.

Jako system CI/CD wybrano Gitlab-CI. Otrzymany pipeline wyglądał mniej więcej tak:

Wdrażanie aplikacji za pomocą Docker Swarm
Strukturalnie gitlab-ci.yml wyglądał następująco

---
variables:
  # minimalna wersja CPU na serwerach, na których wdrażany jest klaster
  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

Warto zauważyć, że kompilacja i testowanie odbywa się na własnym obrazie, na którym są już zainstalowane wszystkie niezbędne pakiety systemowe i dokonane inne konfiguracje.

Chociaż każdy z tych skryptów w zadaniach jest interesujący na swój sposób, nie będę o nich opowiadać, ponieważ opis każdego z nich zajmie znaczną ilość czasu, a to nie jest celem artykułu. Zwrócę jedynie uwagę, że etap wdrażania składa się z sekwencji wywoływania skryptów:

  1. createconfig.py — tworzy plik settings.ini z konfiguracjami komponentów w różnych środowiskach do późniejszego wdrożenia (Preproduction, Production, Testing, …)
  2. install_venv.sh — tworzy wirtualne środowisko dla komponentów py w określonym katalogu i kopiuje je na zdalne serwery
  3. prepare_init.d.py — przygotowuje skrypty uruchamiania i zatrzymywania komponentów na podstawie szablonu
  4. deploy.py — rozkłada i restartuje nowe komponenty

Czas mijał. Etap staging został zastąpiony preproduction i production. Dodano wsparcie produktu na jeszcze jednym dystrybucie (CentOS). Dodano pięć potężnych fizycznych serwerów i kilkanaście wirtualnych. Deweloperom i testerom coraz trudniej było testować swoje zadania w środowisku bardziej zbliżonym do stanu roboczego. W tym czasie stało się jasne, że nie można się bez niego obejść…

Część II

Wdrażanie aplikacji za pomocą Docker Swarm

Nasz klaster to spektakl, system składający się z kilku dziesiątek oddzielnych komponentów, które nie są opisane w Dockerfile. Skonfigurowanie go do wdrożenia w określonym środowisku można zrobić tylko w całości. Naszym zadaniem jest wdrożenie klastra w środowisku staging, aby przetestować go przed testowaniem przedpremierowym.

Teoretycznie może być kilka równocześnie działających klastrów: tyle, ile zadań jest w zakończonym stanie lub bliskim zakończeniu. Moce dostępnych nam serwerów pozwalają na uruchamianie kilku klastrów na każdym serwerze. Każdy klaster staging powinien być izolowany (nie może być nakładania się portów, katalogów itp.).

Najcenniejszym zasobem jest nasz czas, a mieliśmy go niewiele.

Aby szybciej wystartować, wybraliśmy Docker Swarm ze względu na jego prostotę i elastyczność architektury. Pierwszą rzeczą, którą zrobiliśmy, to stworzenie na zdalnych serwerach menedżera i kilku węzłów:

$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
kilqc94pi2upzvabttikrfr5d     nop-test-1     Gotowy               Aktywny                                  19.03.2
jilwe56pl2zvabupryuosdj78     nop-test-2     Gotowy               Aktywny                                  19.03.2
j5a4yz1kr2xke6b1ohoqlnbq5 *   nop-test-3     Gotowy               Aktywny              Lider              19.03.2

Następnie utworzyliśmy sieć:


$ docker network create --driver overlay --subnet 10.10.10.0/24 nw_swarm

Następnie połączyliśmy Gitlab-CI i węzły Swarm w zakresie zdalnego zarządzania węzłami z CI: instalacja certyfikatów, ustawienia zmiennych sekretów oraz konfiguracja usługi Docker na serwerze zarządzającym. To artykuł bardzo zaoszczędziło nam czas.

Następnie dodaliśmy joby do tworzenia i niszczenia stosu w .gitlab-ci.yml.

W .gitlab-ci.yml dodano jeszcze kilka jobów

## 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 w Pipelines dodano dwa przyciski (deploy_staging, stop_staging), które wymagają ręcznej interwencji.

Wdrażanie aplikacji za pomocą Docker Swarm
Nazwa stosu odpowiada nazwie gałęzi i ta unikalność powinna być wystarczająca. Usługi w stosie otrzymują unikalne adresy IP, a porty, katalogi itp. będą odizolowane, ale identyczne od stosu do stosu (ponieważ plik konfiguracyjny jest taki sam dla wszystkich stosów) — co chcieliśmy osiągnąć. Stos (klaster) wdrażamy za pomocą docker-compose.yml, w którym opisano 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

Tu można zobaczyć, że komponenty są połączone jedną siecią (nw_swarm) i są dla siebie dostępne.

Komponenty systemowe (oparte na redis, mysql) są oddzielone od ogólnej puli komponentów dostosowanych (plany obejmują również podział na usługi). Etap wdrażania naszego klastra wygląda jak przekazywanie CMD do naszego jednego dużego skonfigurowanego obrazu i praktycznie nie różni się od opisanego w Części I. Podkreślam różnice:

  • git clone … — otrzymujemy pliki niezbędne do przeprowadzenia wdrożenia (createconfig.py, install_venv.sh itp.)
  • curl… && unzip … — pobieramy i rozpakowujemy artefakty kompilacji (skompilowane narzędzia)

Pozostał tylko jeden nieopisany problem: komponenty, które mają interfejs internetowy, nie są dostępne z przeglądarek programistów. Rozwiązujemy ten problem za pomocą reverse proxy, w ten sposób:

W .gitlab-ci.yml po wdrożeniu stosu klastra dodajemy linię wdrożenia load balancera (który przy commitach, jedynie aktualizuje swoją konfigurację (tworzy nowe pliki konfiguracyjne nginx według szablonu: /etc/nginx/conf.d/${CI_COMMIT_REF_NAME}.conf) — zob. 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 programistów aktualizujemy /etc/hosts; wpisujemy URL do nginx:

10.50.173.106 staging_BRANCH-1831_cluster.dev

Zatem wdrożenie izolowanych klastrów stagingowych zostało zrealizowane i programiści mogą teraz uruchamiać je w dowolnej liczbie wystarczającej do testowania ich zadań.

Kolejne plany:

  • Podzielić nasze komponenty na usługi
  • Utworzyć dla każdego Dockerfile
  • Automatycznie określać mniej obciążone węzły w stosie
  • Przypisywać węzły według wzoru nazwy (a nie używać id jak w artykule)
  • Dodać sprawdzenie, że stos został zniszczony

Osobne podziękowania za artykuł.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster