Distribuisci applicazioni con Docker Swarm

Il sistema di raccomandazione dei contenuti video online su cui stiamo lavorando è uno sviluppo commerciale chiuso ed è tecnicamente un cluster multicomponente di componenti proprietari e open source. Lo scopo di scrivere questo articolo è descrivere l'implementazione del sistema di clustering dello sciame docker per un sito di staging senza interrompere il flusso di lavoro stabilito dei nostri processi in un tempo limitato. La narrazione presentata alla tua attenzione è divisa in due parti. La prima parte descrive CI/CD prima di utilizzare docker swarm e la seconda descrive il processo della sua implementazione. Chi non fosse interessato a leggere la prima parte può tranquillamente passare alla seconda.

Parte I

Nel lontano, lontano anno, era necessario impostare il processo CI / CD il più rapidamente possibile. Una delle condizioni era non utilizzare Docker per la distribuzione componenti sviluppati per diversi motivi:

  • per un funzionamento più affidabile e stabile dei componenti in Produzione (che è, appunto, il requisito di non utilizzare la virtualizzazione)
  • i principali sviluppatori non volevano lavorare con Docker (strano, ma è andata così)
  • secondo le considerazioni ideologiche della direzione R&S

Infrastruttura, stack e requisiti iniziali approssimativi per MVP sono stati presentati come segue:

  • 4 server Intel® X5650 con Debian (una macchina più potente è completamente sviluppata)
  • Lo sviluppo dei propri componenti personalizzati viene effettuato in C ++, Python3
  • Principali strumenti di terze parti utilizzati: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
  • Pipeline per la creazione e il test dei componenti separatamente per il debug e il rilascio

Una delle prime domande che deve essere affrontata nella fase iniziale è come verranno distribuiti i componenti personalizzati in qualsiasi ambiente (CI/CD).

Abbiamo deciso di installare sistematicamente componenti di terze parti e di aggiornarli sistematicamente. Le applicazioni personalizzate sviluppate in C++ o Python possono essere distribuite in diversi modi. Tra questi, ad esempio: creare pacchetti di sistema, inviarli al repository di immagini costruite e quindi installarli sui server. Per un motivo sconosciuto, è stato scelto un altro metodo, vale a dire: utilizzando CI, vengono compilati i file eseguibili dell'applicazione, viene creato un ambiente di progetto virtuale, i moduli py vengono installati da requirements.txt e tutti questi artefatti vengono inviati insieme a configurazioni, script e il ambiente applicativo di accompagnamento ai server. Successivamente, le applicazioni vengono avviate come utente virtuale senza diritti di amministratore.

Gitlab-CI è stato scelto come sistema CI/CD. La pipeline risultante era simile a questa:

Distribuisci applicazioni con Docker Swarm
Strutturalmente, gitlab-ci.yml aveva questo aspetto

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

Vale la pena notare che l'assemblaggio e il test vengono eseguiti sulla propria immagine, dove sono già stati installati tutti i pacchetti di sistema necessari e sono state effettuate altre impostazioni.

Sebbene ciascuno di questi script nei lavori sia interessante a modo suo, ma ovviamente non ne parlerò, la descrizione di ciascuno di essi richiederà molto tempo e questo non è lo scopo dell'articolo. Attirerò la tua attenzione solo sul fatto che la fase di distribuzione consiste in una sequenza di script di chiamata:

  1. createconfig.py - crea un file settings.ini con le impostazioni dei componenti in vari ambienti per la successiva distribuzione (preproduzione, produzione, test, ...)
  2. install_venv.sh - crea un ambiente virtuale per i componenti py in una directory specifica e lo copia sui server remoti
  3. prepare_init.d.py — prepara gli script start-stop per il componente in base al modello
  4. deploy.py - scompone e riavvia nuovi componenti

Il tempo passò. La fase di messa in scena è stata sostituita dalla preproduzione e dalla produzione. Aggiunto il supporto per il prodotto su un'altra distribuzione (CentOS). Aggiunti 5 server fisici più potenti e una dozzina di server virtuali. Ed è diventato sempre più difficile per sviluppatori e tester testare le proprie attività in un ambiente più o meno vicino allo stato di lavoro. In questo momento, è diventato chiaro che era impossibile fare a meno di lui ...

Parte II

Distribuisci applicazioni con Docker Swarm

Quindi, il nostro cluster è un sistema spettacolare di un paio di dozzine di componenti separati che non sono descritti da Dockerfiles. Puoi configurarlo solo per la distribuzione in un ambiente specifico in generale. Il nostro compito è distribuire il cluster in un ambiente di staging per testarlo prima del test pre-rilascio.

Teoricamente, possono esserci diversi cluster in esecuzione contemporaneamente: tanti quante sono le attività nello stato completato o prossime al completamento. Le capacità dei server a nostra disposizione ci consentono di eseguire diversi cluster su ciascun server. Ogni cluster di gestione temporanea deve essere isolato (non devono esserci intersezioni in porte, directory e così via).

La nostra risorsa più preziosa è il nostro tempo, e non ne avevamo molto.

Per un avvio più rapido, abbiamo scelto Docker Swarm per la sua semplicità e flessibilità dell'architettura. La prima cosa che abbiamo fatto è stata creare un gestore e diversi nodi sui server remoti:

$ 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

Successivamente, crea una rete:


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

Successivamente, abbiamo collegato i nodi Gitlab-CI e Swarm in termini di controllo remoto dei nodi da CI: installazione di certificati, impostazione di variabili segrete e configurazione del servizio Docker sul server di controllo. Questo articolo ci ha fatto risparmiare un sacco di tempo.

Successivamente, abbiamo aggiunto lavori di creazione e distruzione dello stack a .gitlab-ci .yml.

Alcuni altri lavori sono stati aggiunti a .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

Dallo snippet di codice precedente, puoi vedere che due pulsanti (deploy_staging, stop_staging) sono stati aggiunti a Pipelines, richiedendo un'azione manuale.

Distribuisci applicazioni con Docker Swarm
Il nome dello stack corrisponde al nome del ramo e questa univocità dovrebbe essere sufficiente. I servizi nello stack ricevono indirizzi IP univoci e porte, directory, ecc. sarà isolato, ma lo stesso da stack a stack (poiché il file di configurazione è lo stesso per tutti gli stack) - quello che volevamo. Distribuiamo lo stack (cluster) utilizzando finestra mobile-compose.yml, che descrive il nostro cluster.

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

Qui puoi vedere che i componenti sono collegati da una rete (nw_swarm) e sono disponibili l'uno per l'altro.

I componenti di sistema (basati su redis, mysql) sono separati dal pool generale di componenti personalizzati (nei piani e quelli personalizzati sono divisi come servizi). La fase di distribuzione del nostro cluster sembra passare CMD nella nostra unica grande immagine configurata e, in generale, praticamente non differisce dalla distribuzione descritta nella Parte I. Metterò in evidenza le differenze:

  • git clone... - ottieni i file necessari per la distribuzione (createconfig.py, install_venv.sh, ecc.)
  • arriccia... && decomprimi... - scarica e decomprimi gli artefatti di build (utilità compilate)

C'è solo un problema ancora non descritto: i componenti che hanno un'interfaccia web non sono accessibili dai browser degli sviluppatori. Risolviamo questo problema utilizzando il proxy inverso, quindi:

In .gitlab-ci.yml, dopo aver distribuito lo stack del cluster, aggiungiamo la linea di distribuzione del bilanciatore (che, quando esegue il commit, aggiorna solo la sua configurazione (crea nuovi file di configurazione nginx secondo il modello: /etc/nginx/conf. d/${CI_COMMIT_REF_NAME}.conf) - vedere il codice 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

Sui computer di sviluppo, aggiornare /etc/hosts; prescrivere l'URL a nginx:

10.50.173.106 staging_BRANCH-1831_cluster.dev

Pertanto, è stata implementata la distribuzione di cluster di staging isolati e gli sviluppatori possono ora eseguirli in qualsiasi numero sufficiente per controllare le loro attività.

Progetti futuri:

  • Separa i nostri componenti come servizi
  • Avere per ogni Dockerfile
  • Rileva automaticamente i nodi meno caricati nello stack
  • Specifica i nodi per modello di nome (piuttosto che usare id come nell'articolo)
  • Aggiungi un controllo che la pila sia distrutta
  • ...

Un ringraziamento speciale per Articolo.

Fonte: habr.com

Aggiungi un commento