Anwendungen mit Docker Swarm bereitstellen

Das Online-Empfehlungssystem für Video-Inhalte, an dem wir arbeiten, ist ein geschlossenes kommerzielles Projekt und stellt technisch einen mehrkomponentigen Cluster aus proprietären und Open-Source-Komponenten dar. Ziel dieses Artikels ist es, die Implementierung eines Docker Swarm-Clustering-Systems für die Staging-Umgebung zu beschreiben, ohne den bestehenden Workflow unserer Prozesse unter zeitlichen Einschränkungen zu beeinträchtigen. Die Ihnen präsentierte Erzählung ist in zwei Teile gegliedert. Der erste Teil beschreibt CI/CD vor der Nutzung von Docker Swarm, während der zweite den Prozess der Implementierung behandelt. Wer an der ersten Teil nicht interessiert ist, kann direkt zum zweiten übergehen.

Teil I

Im weit entfernten Jahr war es notwendig, den CI/CD-Prozess so schnell wie möglich einzurichten. Eine der Bedingungen war, Docker nicht zu verwenden für die Bereitstellung der entwickelten Komponenten aus mehreren Gründen:

  • um eine zuverlässigere und stabilere Funktion der Komponenten in der Produktion zu gewährleisten (d.h. im Grunde die Anforderung, Virtualisierung nicht zu verwenden)
  • die leitenden Entwickler wollten nicht mit Docker arbeiten (seltsam, aber es war so)
  • aus ideologischen Gründen der Geschäftsführung der F&E

Die Infrastruktur, der Stack und die beispielhaften Anforderungen für ein MVP wurden wie folgt dargestellt:

  • 4 Server Intel® X5650 mit Debian (ein leistungsstärkerer Server vollständig für die Entwicklung)
  • Die Entwicklung eigener maßgeschneiderter Komponenten erfolgt in C++, Python3.
  • Die wichtigsten 3rd Party-Tools, die verwendet werden: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
  • Build- und Test-Pipelines für Komponenten separat für Debug und Release.

Eine der ersten Fragen, die in der Anfangsphase geklärt werden muss, ist, wie die Bereitstellung der maßgeschneiderten Komponenten in einer Umgebung (CI/CD) erfolgen wird.

Drittanbieter-Komponenten werden systematisch installiert und aktualisiert. Für benutzerdefinierte Anwendungen, die in C++ oder Python entwickelt werden, gibt es mehrere Bereitstellungsmöglichkeiten. Dazu gehört beispielsweise: die Erstellung von Systempaketen, das Hochladen in ein Repository für zusammengebaute Images und die anschließende Installation auf Servern. Aus nicht mehr nachvollziehbaren Gründen wurde jedoch ein anderer Ansatz gewählt: Mit Hilfe von CI werden die ausführbaren Dateien der Anwendungen kompiliert, eine virtuelle Projektumgebung erstellt, die py-Module aus der requirements.txt installiert und all diese Artefakte zusammen mit Konfigurationen, Skripten und der dazugehörigen Anwendungsumgebung auf die Server übertragen. Anschließend werden die Anwendungen von einem virtuellen Benutzer ohne Administratorrechte gestartet.

Als CI/CD-System wurde Gitlab-CI ausgewählt. Die resultierende Pipeline sah ungefähr so aus:

Anwendungen mit Docker Swarm bereitstellen
Strukturell sah die gitlab-ci.yml wie folgt aus

---
variablen:
  # minimale CPU-Version auf Servern, auf denen der Cluster bereitgestellt wird
  CMAKE_CPUTYPE: "westmere"

  DEBIAN: "MYREGISTRY:5000/debian:latest"

before_script:
  - eval $(ssh-agent -s)
  - ssh-add  ~/.ssh/config

stufen:
  - build
  - testing
  - deploy

debug.debian:
  stufe: build
  bild: $DEBIAN
  skript:
    - cd builds/release && ./build.sh
    pfade:
      - bin/
      - builds/release/bin/
    wann: immer
release.debian:
  stufe: build
  bild: $DEBIAN
  skript:
    - cd builds/release && ./build.sh
    pfade:
      - bin/
      - builds/release/bin/
    wann: immer

## Teststufe
tests.codestyle:
  stufe: testing
  bild: $DEBIAN
  abhängigkeiten:
    - release.debian
  skript:
    - /bin/bash run_tests.sh -t codestyle -b "${CI_COMMIT_REF_NAME}_codestyle"
tests.debug.debian:
  stufe: testing
  bild: $DEBIAN
  abhängigkeiten:
    - debug.debian
  skript:
    - /bin/bash run_tests.sh -e codestyle/test_pylint.py -b "${CI_COMMIT_REF_NAME}_debian_debug"
  artefakte:
    pfade:
      - run_tests/username/
    wann: immer
    ablaufen_in: 1 Woche
tests.release.debian:
  stufe: testing
  bild: $DEBIAN
  abhängigkeiten:
    - release.debian
  skript:
    - /bin/bash run_tests.sh -e codestyle/test_pylint.py -b "${CI_COMMIT_REF_NAME}_debian_release"
  artefakte:
    pfade:
      - run_tests/username/
    wann: immer
    ablaufen_in: 1 Woche

## Staging-Stufe
deploy_staging:
  stufe: deploy
  umgebung: staging
  bild: $DEBIAN
  abhängigkeiten:
    - release.debian
  skript:
    - 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
  wann: manuell

Es ist wichtig zu beachten, dass die Zusammenstellung und das Testen in einem eigenen Image erfolgt, in dem bereits alle notwendigen Systempakete installiert sind und weitere Einstellungen vorgenommen wurden.

Obwohl jedes dieser Skripte in den Jobs auf seine Weise interessant ist, werde ich nicht näher auf sie eingehen, da die Beschreibung jedes einzelnen erheblich Zeit in Anspruch nehmen würde und das nicht das Ziel des Artikels ist. Ich möchte lediglich darauf hinweisen, dass die Deployment-Phase aus einer Reihe von Skripterufen besteht:

  1. createconfig.py — erstellt die Datei settings.ini mit den Konfigurationen der Komponenten in verschiedenen Umgebungen für das anschließende Deployment (Preproduction, Production, Testing, …)
  2. install_venv.sh — erstellt eine virtuelle Umgebung für Python-Komponenten in einem bestimmten Verzeichnis und kopiert sie auf die Remote-Server
  3. prepare_init.d.py — bereitet die Start- und Stoppskripte der Komponenten auf Grundlage einer Vorlage vor
  4. deploy.py — verteilt und startet die neuen Komponenten neu

Die Zeit verging. Die Staging-Phase wurde durch Preproduction und Production ersetzt. Zudem wurde das Produkt auch für eine weitere Distribution (CentOS) unterstützt. Fünf leistungsstarke physische Server und ein Dutzend virtuelle Server kamen hinzu. Entwicklern und Testern wurde es zunehmend schwieriger, ihre Aufgaben in einer Umgebung zu testen, die dem Produktionszustand einigermaßen nahekam. Zu diesem Zeitpunkt wurde klar, dass man nicht ohne sie auskam...

Teil II

Anwendungen mit Docker Swarm bereitstellen

Unser Cluster stellt ein beeindruckendes System aus mehreren Dutzend separaten Komponenten dar, die nicht durch Dockerfiles beschrieben werden. Es kann nur insgesamt für das bestimmte Deploy-Umfeld konfiguriert werden. Unsere Aufgabe besteht darin, den Cluster in der Staging-Umgebung zu deployen, um ihn vor den Pre-Release-Tests auszuprobieren.

Theoretisch können mehrere Cluster gleichzeitig betrieben werden: so viele wie Aufgaben im abgeschlossenen oder nahezu abgeschlossenen Zustand. Die Ressourcen der uns zur Verfügung stehenden Server ermöglichen das Starten mehrerer Cluster auf jedem Server. Jeder Staging-Cluster muss isoliert sein (es darf keine Überlappungen bei Ports, Verzeichnissen usw. geben).

Die wertvollste Ressource ist unsere Zeit, und davon hatten wir nicht viel.

Für einen schnelleren Start haben wir Docker Swarm aufgrund seiner Einfachheit und Flexibilität der Architektur gewählt. Das erste, was wir gemacht haben, war, einen Manager und mehrere Nodes auf den Remote-Servern zu erstellen:

$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
kilqc94pi2upzvabttikrfr5d     nop-test-1     Bereit               Aktiv                                  19.03.2
jilwe56pl2zvabupryuosdj78     nop-test-2     Bereit               Aktiv                                  19.03.2
j5a4yz1kr2xke6b1ohoqlnbq5 *   nop-test-3     Bereit               Aktiv              Leiter              19.03.2

Dann haben wir ein Netzwerk erstellt:


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

Anschließend haben wir Gitlab-CI und die Swarm-Nodes in Bezug auf die Remote-Verwaltung der Nodes aus der CI verbunden: Installation von Zertifikaten, Konfiguration von geheimen Variablen sowie die Einrichtung des Docker-Dienstes auf dem verwaltenden Server. Dies Artikel hat uns viel Zeit gespart.

Dann haben wir Jobs zur Erstellung und Zerstörung des Stacks in der .gitlab-ci.yml hinzugefügt.

In der .gitlab-ci.yml wurden einige zusätzliche Jobs hinzugefügt.

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

Aus dem obigen Code-Ausschnitt ist ersichtlich, dass in den Pipelines zwei Schaltflächen (deploy_staging, stop_staging) hinzugefügt wurden, die manuelles Eingreifen erfordern.

Anwendungen mit Docker Swarm bereitstellen
Der Name des Stacks entspricht dem Namen des Branches, und diese Einzigartigkeit sollte ausreichend sein. Die Dienste im Stack erhalten einzigartige IP-Adressen, während Ports, Verzeichnisse usw. isoliert, aber von Stack zu Stack identisch sind (da die Konfigurationsdatei für alle Stacks gleich ist) – genau das haben wir angestrebt. Den Stack (Cluster) richten wir mit Hilfe von docker-compose.yml, in dem unser Cluster beschrieben ist.

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

Hier ist zu erkennen, dass die Komponenten durch ein Netzwerk (nw_swarm) verbunden und untereinander zugänglich sind.

Die systematischen Komponenten (basierend auf Redis, MySQL) sind vom gemeinsamen Pool der benutzerdefinierten Komponenten getrennt (in Planung ist auch, diese als separate Dienste zu splitten). Die Deploy-Phase unseres Clusters sieht wie der Transfer von CMD in unser großes, konfiguriertes Image aus und unterscheidet sich im Grunde kaum von dem in Teil I beschriebenen Deployment. Ich möchte die Unterschiede betonen:

  • git clone … — wir erhalten die notwendigen Dateien für das Deployment (createconfig.py, install_venv.sh usw.)
  • curl… && unzip … — wir laden die Build-Artefakte herunter und entpacken sie (kompilierte Tools)

Es bleibt nur noch ein bisher nicht beschriebenes Problem: Die Komponenten mit Web-Interface sind nicht aus den Browsern der Entwickler zugänglich. Wir lösen dieses Problem mit einem Reverse Proxy wie folgt:

In der .gitlab-ci.yml fügen wir nach dem Deployment des Cluster-Stacks eine Zeile zum Deployment des Load Balancers hinzu (der bei Commits nur seine Konfiguration aktualisiert (neue nginx-Konfigurationsdateien nach dem Muster: /etc/nginx/conf.d/${CI_COMMIT_REF_NAME}.conf erstellt) — siehe code 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

Auf den Entwicklercomputern aktualisieren wir /etc/hosts; wir schreiben die URL für nginx hinein:

10.50.173.106 staging_BRANCH-1831_cluster.dev

So, das Deployment isolierter Staging-Cluster ist umgesetzt, und die Entwickler können diese nun in ausreichender Anzahl für ihre Aufgaben überprüfen.

Weitere Pläne:

  • Unsere Komponenten als Services aufteilen
  • Für jedes Dockerfile eine separate Datei anlegen
  • Automatisch weniger ausgelastete Knoten im Stack bestimmen
  • Knoten nach Namensmuster festlegen (statt ID wie im Artikel zu verwenden)
  • Überprüfung hinzufügen, dass der Stack gelöscht wurde

Besonderer Dank für den Artikel.

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster