Wdrażaj aplikacje za pomocą Docker Swarm

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:

Wdrażaj aplikacje za pomocą Docker Swarm
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:

  1. utwórzkonfigurację.py - tworzy plik settings.ini z ustawieniami komponentów w różnych środowiskach do późniejszego wdrożenia (preprodukcja, produkcja, testowanie, ...)
  2. install_venv.sh - tworzy wirtualne środowisko dla komponentów py w określonym katalogu i kopiuje je na zdalne serwery
  3. przygotowanie_init.d.py — przygotowuje skrypty start-stop dla komponentu na podstawie szablonu
  4. 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

Wdrażaj aplikacje za pomocą Docker Swarm

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 artykuł zaoszczędziło nam dużo czasu.

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.

Wdrażaj aplikacje za pomocą Docker Swarm
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

Dodaj komentarz