Déployer des applications avec Docker Swarm

Le système de recommandation de contenu vidéo en ligne sur lequel nous travaillons est un développement commercial fermé et est techniquement un cluster multi-composants de composants propriétaires et open source. Le but de la rédaction de cet article est de décrire la mise en œuvre du système de clustering docker swarm pour un site intermédiaire sans perturber le flux de travail établi de nos processus dans un temps limité. Le récit présenté à votre attention est divisé en deux parties. La première partie décrit le CI/CD avant d'utiliser docker swarm, et la seconde décrit le processus de sa mise en place. Ceux qui ne sont pas intéressés par la lecture de la première partie peuvent passer en toute sécurité à la seconde.

Partie I

De retour dans l'année lointaine, lointaine, il était nécessaire de mettre en place le processus CI / CD le plus rapidement possible. Une des conditions était de ne pas utiliser Docker pour le déploiement composants développés pour plusieurs raisons :

  • pour un fonctionnement plus fiable et stable des composants en production (c'est-à-dire, en fait, l'obligation de ne pas utiliser la virtualisation)
  • les principaux développeurs ne voulaient pas travailler avec Docker (bizarre, mais c'était comme ça)
  • selon les considérations idéologiques de la direction R&D

L'infrastructure, la pile et les exigences initiales approximatives pour MVP ont été présentées comme suit :

  • 4 serveurs Intel® X5650 avec Debian (une machine plus puissante est entièrement développée)
  • Le développement de propres composants personnalisés est effectué en C ++, Python3
  • Principaux outils tiers utilisés : Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
  • Pipelines pour créer et tester des composants séparément pour le débogage et la publication

L'une des premières questions qui doit être abordée au stade initial est de savoir comment les composants personnalisés seront déployés dans n'importe quel environnement (CI / CD).

Nous avons décidé d'installer systématiquement des composants tiers et de les mettre à jour systématiquement. Les applications personnalisées développées en C++ ou Python peuvent être déployées de plusieurs manières. Parmi eux, par exemple : créer des packages système, les envoyer au référentiel d'images construites, puis les installer sur des serveurs. Pour une raison inconnue, une autre méthode a été choisie, à savoir : à l'aide de CI, les fichiers exécutables de l'application sont compilés, un environnement de projet virtuel est créé, les modules py sont installés à partir de requirements.txt, et tous ces artefacts sont envoyés avec les configurations, les scripts et le accompagnement de l'environnement applicatif vers les serveurs. Ensuite, les applications sont lancées en tant qu'utilisateur virtuel sans droits d'administrateur.

Gitlab-CI a été choisi comme système CI/CD. Le pipeline résultant ressemblait à ceci :

Déployer des applications avec Docker Swarm
Structurellement, gitlab-ci.yml ressemblait à ceci

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

Il convient de noter que l'assemblage et les tests sont effectués sur sa propre image, où tous les packages système nécessaires ont déjà été installés et d'autres paramètres ont été définis.

Bien que chacun de ces scripts dans les métiers soit intéressant à sa manière, mais bien sûr je n'en parlerai pas, la description de chacun d'eux prendra beaucoup de temps et ce n'est pas le but de l'article. J'attire seulement votre attention sur le fait que l'étape de déploiement consiste en une séquence de scripts d'appel :

  1. créerconfig.py - crée un fichier settings.ini avec les paramètres des composants dans différents environnements pour un déploiement ultérieur (préproduction, production, test, ...)
  2. install_venv.sh - crée un environnement virtuel pour les composants py dans un répertoire spécifique et le copie sur des serveurs distants
  3. préparer_init.d.py — prépare les scripts start-stop pour le composant en fonction du modèle
  4. déployer.py - décompose et redémarre de nouveaux composants

Le temps passait. L'étape de la mise en scène a été remplacée par la préproduction et la production. Ajout de la prise en charge du produit sur une distribution supplémentaire (CentOS). Ajout de 5 serveurs physiques plus puissants et d'une dizaine de serveurs virtuels. Et il devenait de plus en plus difficile pour les développeurs et les testeurs de tester leurs tâches dans un environnement plus ou moins proche de l'état de fonctionnement. À ce moment-là, il est devenu clair qu'il était impossible de se passer de lui ...

Partie II

Déployer des applications avec Docker Swarm

Ainsi, notre cluster est un système spectaculaire de quelques dizaines de composants séparés qui ne sont pas décrits par Dockerfiles. Vous pouvez uniquement le configurer pour un déploiement dans un environnement spécifique en général. Notre tâche consiste à déployer le cluster dans un environnement intermédiaire pour le tester avant les tests de pré-version.

Théoriquement, plusieurs clusters peuvent s'exécuter simultanément : autant qu'il y a de tâches à l'état terminé ou proche de l'achèvement. Les capacités des serveurs à notre disposition nous permettent de faire tourner plusieurs clusters sur chaque serveur. Chaque cluster intermédiaire doit être isolé (il ne doit y avoir aucune intersection dans les ports, les répertoires, etc.).

Notre ressource la plus précieuse est notre temps, et nous n'en avions pas beaucoup.

Pour un démarrage plus rapide, nous avons choisi Docker Swarm en raison de sa simplicité et de la flexibilité de son architecture. La première chose que nous avons faite a été de créer un gestionnaire et plusieurs nœuds sur les serveurs distants :

$ 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

Ensuite, créez un réseau :


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

Ensuite, nous avons connecté les nœuds Gitlab-CI et Swarm en termes de contrôle à distance des nœuds depuis CI : installation de certificats, définition de variables secrètes et configuration du service Docker sur le serveur de contrôle. Celui-ci article nous a fait gagner beaucoup de temps.

Ensuite, nous avons ajouté des tâches de création et de destruction de pile à .gitlab-ci .yml.

Quelques tâches supplémentaires ont été ajoutées à .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

À partir de l'extrait de code ci-dessus, vous pouvez voir que deux boutons (deploy_staging, stop_staging) ont été ajoutés aux pipelines, nécessitant une action manuelle.

Déployer des applications avec Docker Swarm
Le nom de la pile correspond au nom de la branche et cette unicité devrait être suffisante. Les services de la pile reçoivent des adresses IP uniques, des ports, des répertoires, etc. sera isolé, mais identique d'une pile à l'autre (car le fichier de configuration est le même pour toutes les piles) - ce que nous voulions. Nous déployons la pile (cluster) en utilisant docker-compose.yml, qui décrit notre cluster.

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

Ici, vous pouvez voir que les composants sont connectés par un réseau (nw_swarm) et sont disponibles les uns pour les autres.

Les composants système (basés sur redis, mysql) sont séparés du pool général de composants personnalisés (dans les plans et les composants personnalisés sont divisés en services). L'étape de déploiement de notre cluster ressemble à la transmission de CMD dans notre seule grande image configurée et, en général, ne diffère pratiquement pas du déploiement décrit dans la partie I. Je soulignerai les différences :

  • cloner git... - obtenir les fichiers nécessaires au déploiement (createconfig.py, install_venv.sh, etc.)
  • enrouler... && décompresser... - télécharger et décompresser les artefacts de construction (utilitaires compilés)

Il n'y a qu'un seul problème encore non décrit : les composants qui ont une interface Web ne sont pas accessibles depuis les navigateurs des développeurs. Nous résolvons ce problème en utilisant un proxy inverse, ainsi :

Dans .gitlab-ci.yml, après avoir déployé la pile du cluster, nous ajoutons la ligne de déploiement de l'équilibreur (qui, lors des validations, ne fait que mettre à jour sa configuration (crée de nouveaux fichiers de configuration nginx selon le modèle : /etc/nginx/conf. d/${CI_COMMIT_REF_NAME}.conf) - voir le 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

Sur les ordinateurs de développement, mettez à jour /etc/hosts ; prescrire l'url à nginx :

10.50.173.106 staging_BRANCH-1831_cluster.dev

Ainsi, le déploiement de clusters de staging isolés a été implémenté et les développeurs peuvent désormais les exécuter en nombre suffisant pour vérifier leurs tâches.

Plans futurs:

  • Séparer nos composants en tant que services
  • Avoir pour chaque Dockerfile
  • Détecter automatiquement les nœuds les moins chargés dans la pile
  • Spécifiez les nœuds par modèle de nom (plutôt que d'utiliser l'identifiant comme dans l'article)
  • Ajouter une vérification que la pile est détruite
  • ...

Remerciements particuliers pour статью.

Source: habr.com

Ajouter un commentaire