Distribuera applikationer med Docker Swarm

Rekommendationssystemet för onlinevideoinnehåll vi arbetar med är en sluten kommersiell utveckling och är tekniskt sett ett flerkomponentkluster av proprietära och öppen källkodskomponenter. Syftet med att skriva den här artikeln är att beskriva implementeringen av docker-svärmklustringssystemet för en mellanstation utan att störa det etablerade arbetsflödet för våra processer under en begränsad tid. Berättelsen som presenteras för din uppmärksamhet är uppdelad i två delar. Den första delen beskriver CI / CD innan du använder docker swarm, och den andra beskriver processen för dess implementering. Den som inte är intresserad av att läsa den första delen kan lugnt gå vidare till den andra.

Del I

Tillbaka i det avlägsna, avlägsna året var det nödvändigt att sätta upp CI / CD-processen så snabbt som möjligt. Ett av villkoren var att inte använda Docker för utplacering utvecklade komponenter av flera anledningar:

  • för mer tillförlitlig och stabil drift av komponenter i produktion (det vill säga kravet att inte använda virtualisering)
  • ledande utvecklare ville inte arbeta med Docker (konstigt, men det var så det var)
  • enligt FoU-ledningens ideologiska överväganden

Infrastruktur, stack och ungefärliga initiala krav för MVP presenterades enligt följande:

  • 4 Intel® X5650-servrar med Debian (en mer kraftfull maskin är fullt utvecklad)
  • Utveckling av egna anpassade komponenter utförs i C++, Python3
  • De viktigaste verktygen från tredje part som används: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
  • Rörledningar för att bygga och testa komponenter separat för debug och release

En av de första frågorna som måste lösas i det inledande skedet är hur anpassade komponenter kommer att distribueras i vilken miljö som helst (CI/CD).

Vi bestämde oss för att installera tredjepartskomponenter systemiskt och uppdatera dem systemiskt. Anpassade applikationer utvecklade i C++ eller Python kan distribueras på flera sätt. Bland dem, till exempel: skapa systempaket, skicka dem till arkivet för byggda bilder och sedan installera dem på servrar. Av okänd anledning valdes en annan metod, nämligen: med hjälp av CI kompileras applikationskörbara filer, en virtuell projektmiljö skapas, py-moduler installeras från requirements.txt och alla dessa artefakter skickas tillsammans med konfigurationer, skript och medföljande applikationsmiljö till servrar. Därefter lanseras applikationer som en virtuell användare utan administratörsrättigheter.

Gitlab-CI valdes som CI/CD-system. Den resulterande pipelinen såg ut ungefär så här:

Distribuera applikationer med Docker Swarm
Strukturellt såg gitlab-ci.yml ut så här

---
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

Det är värt att notera att montering och testning utförs på sin egen bild, där alla nödvändiga systempaket redan har installerats och andra inställningar har gjorts.

Även om vart och ett av dessa manus i jobb är intressant på sitt eget sätt, men jag kommer naturligtvis inte att prata om dem. Beskrivningen av var och en av dem kommer att ta mycket tid och detta är inte syftet med artikeln. Jag kommer bara att uppmärksamma er på det faktum att implementeringsstadiet består av en sekvens av anropsskript:

  1. createconfig.py - skapar en settings.ini-fil med komponentinställningar i olika miljöer för efterföljande distribution (förproduktion, produktion, testning, ...)
  2. install_venv.sh - skapar en virtuell miljö för py-komponenter i en specifik katalog och kopierar den till fjärrservrar
  3. prepare_init.d.py — förbereder start-stop-skript för komponenten baserat på mallen
  4. deploy.py - bryter ner och startar om nya komponenter

Tiden gick. Iscensättningsstadiet ersattes av förproduktion och produktion. Lade till stöd för produkten på ytterligare en distribution (CentOS). Lade till 5 mer kraftfulla fysiska servrar och ett dussin virtuella. Och det blev allt svårare för utvecklare och testare att testa sina uppgifter i en miljö mer eller mindre nära arbetstillståndet. Vid denna tidpunkt blev det klart att det var omöjligt att klara sig utan honom ...

Del II

Distribuera applikationer med Docker Swarm

Så vårt kluster är ett spektakulärt system med ett par dussin separata komponenter som inte beskrivs av Dockerfiles. Du kan bara konfigurera den för distribution till en specifik miljö i allmänhet. Vår uppgift är att distribuera klustret i en iscensättningsmiljö för att testa det före pre-release-testning.

Teoretiskt kan det finnas flera kluster som körs samtidigt: så många som det finns uppgifter i slutfört tillstånd eller nära slutförande. Kapaciteten hos de servrar som står till vårt förfogande gör att vi kan köra flera kluster på varje server. Varje iscensättningskluster måste vara isolerat (det får inte finnas någon korsning i portar, kataloger, etc.).

Vår mest värdefulla resurs är vår tid, och vi hade inte mycket av den.

För en snabbare start valde vi Docker Swarm på grund av dess enkelhet och arkitekturflexibilitet. Det första vi gjorde var att skapa en manager och flera noder på fjärrservrarna:

$ 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

Skapa sedan ett nätverk:


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

Därefter kopplade vi Gitlab-CI och Swarm-noder vad gäller fjärrstyrning av noder från CI: installation av certifikat, inställning av hemliga variabler och inställning av Docker-tjänsten på kontrollservern. Den här artikel sparade oss mycket tid.

Därefter lade vi till stackskapande och destruktionsjobb till .gitlab-ci .yml.

Några fler jobb har lagts till i .gitlab-ci .yml

## 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

Från ovanstående kodavsnitt kan du se att två knappar (deploy_staging, stop_staging) har lagts till i Pipelines, vilket kräver manuell åtgärd.

Distribuera applikationer med Docker Swarm
Stacknamnet matchar filialnamnet och denna unikhet borde vara tillräcklig. Tjänster i stacken får unika ip-adresser och portar, kataloger etc. kommer att vara isolerade, men samma från stack till stack (eftersom konfigurationsfilen är densamma för alla stackar) - vad vi ville ha. Vi distribuerar stacken (klustret) med hjälp av docker-compose.yml, som beskriver vårt kluster.

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

Här kan du se att komponenterna är sammankopplade av ett nätverk (nw_swarm) och är tillgängliga för varandra.

Systemkomponenter (baserade på redis, mysql) är separerade från den allmänna poolen av anpassade komponenter (i planer och anpassade är de uppdelade som tjänster). Implementeringsstadiet för vårt kluster ser ut som att överföra CMD till vår enda stora konfigurerade bild och skiljer sig i allmänhet praktiskt taget inte från den distribution som beskrivs i del I. Jag kommer att belysa skillnaderna:

  • git klon... - hämta filerna som behövs för att distribuera (createconfig.py, install_venv.sh, etc.)
  • lock... && dra upp... - ladda ner och packa upp byggartefakter (kompilerade verktyg)

Det finns bara ett ännu obeskrivet problem: komponenter som har ett webbgränssnitt är inte tillgängliga från utvecklarnas webbläsare. Vi löser det här problemet med omvänd proxy, så här:

I .gitlab-ci.yml, efter att ha distribuerat klusterstacken, lägger vi till raden för att distribuera balancern (som, när den commits, bara uppdaterar dess konfiguration (skapar nya nginx-konfigurationsfiler enligt mallen: /etc/nginx/conf. d/${CI_COMMIT_REF_NAME}.conf) - se docker-compose-nginx.yml kod)

    - 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

Uppdatera /etc/hosts på utvecklingsdatorerna; ordinera url till nginx:

10.50.173.106 staging_BRANCH-1831_cluster.dev

Så, utplaceringen av isolerade iscensättningskluster har implementerats och utvecklare kan nu köra dem i ett tillräckligt antal för att kontrollera deras uppgifter.

Framtida planer:

  • Separera våra komponenter som tjänster
  • Har för varje Dockerfile
  • Upptäck automatiskt mindre belastade noder i stacken
  • Ange noder efter namnmönster (istället för att använda id som i artikeln)
  • Lägg till en kontroll att stacken är förstörd
  • .

Speciellt tack för Artikel.

Källa: will.com

Lägg en kommentar