Deploy aplicațiilor folosind Docker Swarm

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:

Deploy aplicațiilor folosind Docker Swarm
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:

  1. createconfig.py — creează fișierul settings.ini cu setările componentelor în diferite medii pentru următoarea desfășurare (Preproduction, Production, Testing, …)
  2. install_venv.sh — creează un mediu virtual pentru componentele py într-un director specific și îl copiază pe serverele de la distanță
  3. prepare_init.d.py — pregătește scripturile de start/dop pentru componente pe baza unui șablon
  4. 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

Deploy aplicațiilor folosind Docker Swarm

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 articol 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ă.

Deploy aplicațiilor folosind Docker Swarm
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 articol.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster