Implantar aplicativos com o Docker Swarm

O sistema de recomendação de conteúdo de vídeo on-line no qual estamos trabalhando é um desenvolvimento comercial fechado e é tecnicamente um cluster multicomponente de componentes proprietários e de código aberto. O objetivo de escrever este artigo é descrever a implementação do sistema de agrupamento docker swarm para um site de teste sem interromper o fluxo de trabalho estabelecido de nossos processos em um tempo limitado. A narrativa apresentada a sua atenção é dividida em duas partes. A primeira parte descreve o CI/CD antes de usar o docker swarm, e a segunda descreve o processo de sua implementação. Aqueles que não estão interessados ​​em ler a primeira parte podem passar com segurança para a segunda.

Parte I

Em um ano distante, distante, era necessário configurar o processo CI / CD o mais rápido possível. Uma das condições era não usar o Docker para implantação componentes desenvolvidos por vários motivos:

  • para uma operação mais confiável e estável dos componentes em Produção (isso é, de fato, o requisito para não usar a virtualização)
  • os principais desenvolvedores não queriam trabalhar com o Docker (estranho, mas era assim)
  • de acordo com as considerações ideológicas da gestão de P&D

Infraestrutura, pilha e requisitos iniciais aproximados para MVP foram apresentados a seguir:

  • 4 servidores Intel® X5650 com Debian (mais uma máquina poderosa totalmente desenvolvida)
  • O desenvolvimento de componentes customizados próprios é realizado em C++, Python3
  • Principais ferramentas de terceiros utilizadas: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
  • Pipelines para criar e testar componentes separadamente para depuração e lançamento

Uma das primeiras questões que precisam ser abordadas no estágio inicial é como os componentes customizados serão implantados em qualquer ambiente (CI/CD).

Decidimos instalar componentes de terceiros sistematicamente e atualizá-los sistematicamente. Aplicativos personalizados desenvolvidos em C++ ou Python podem ser implantados de várias maneiras. Entre eles, por exemplo: criar pacotes do sistema, enviá-los para o repositório de imagens construídas e depois instalá-los nos servidores. Por uma razão desconhecida, outro método foi escolhido, a saber: usando CI, os arquivos executáveis ​​do aplicativo são compilados, um ambiente de projeto virtual é criado, os módulos py são instalados a partir de requirements.txt e todos esses artefatos são enviados junto com configs, scripts e o ambiente de aplicação que acompanha os servidores. Em seguida, os aplicativos são iniciados como um usuário virtual sem direitos de administrador.

Gitlab-CI foi escolhido como o sistema CI/CD. O pipeline resultante ficou mais ou menos assim:

Implantar aplicativos com o Docker Swarm
Estruturalmente, gitlab-ci.yml ficou assim

---
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 ressaltar que a montagem e os testes são realizados em imagem própria, onde todos os pacotes de sistema necessários já foram instalados e outras configurações feitas.

Embora cada um desses scripts em trabalhos seja interessante à sua maneira, mas é claro que não vou falar sobre eles, a descrição de cada um deles levará muito tempo e não é esse o objetivo do artigo. Vou apenas chamar sua atenção para o fato de que o estágio de implantação consiste em uma sequência de scripts de chamada:

  1. createconfig.py - cria um arquivo settings.ini com configurações de componentes em vários ambientes para implantação posterior (pré-produção, produção, teste, ...)
  2. instalar_venv.sh - cria um ambiente virtual para componentes py em um diretório específico e o copia para servidores remotos
  3. prepare_init.d.py — prepara scripts start-stop para o componente com base no modelo
  4. implantar.py - decompõe e reinicia novos componentes

Tempo passou. A etapa de encenação foi substituída pela pré-produção e produção. Adicionado suporte ao produto em mais uma distribuição (CentOS). Adicionados 5 servidores físicos mais poderosos e uma dúzia de servidores virtuais. E tornou-se cada vez mais difícil para desenvolvedores e testadores testar suas tarefas em um ambiente mais ou menos próximo ao estado de trabalho. Nessa época, ficou claro que era impossível ficar sem ele ...

Parte II

Implantar aplicativos com o Docker Swarm

Portanto, nosso cluster é um sistema espetacular de algumas dezenas de componentes separados que não são descritos por Dockerfiles. Você só pode configurá-lo para implantação em um ambiente específico em geral. Nossa tarefa é implantar o cluster em um ambiente de preparação para testá-lo antes do teste de pré-lançamento.

Teoricamente, pode haver vários clusters em execução simultaneamente: tantos quantos forem as tarefas no estado concluído ou próximo da conclusão. As capacidades dos servidores à nossa disposição permitem-nos executar vários clusters em cada servidor. Cada cluster de preparação deve ser isolado (não deve haver interseção em portas, diretórios, etc.).

Nosso recurso mais valioso é o nosso tempo, e não tínhamos muito dele.

Para um início mais rápido, escolhemos o Docker Swarm devido à sua simplicidade e flexibilidade de arquitetura. A primeira coisa que fizemos foi criar um gerenciador e vários nós nos servidores remotos:

$ 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

Em seguida, crie uma rede:


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

Em seguida, conectamos os nós Gitlab-CI e Swarm em termos de controle remoto de nós do CI: instalação de certificados, configuração de variáveis ​​secretas e configuração do serviço Docker no servidor de controle. Este artigo nos poupou muito tempo.

Em seguida, adicionamos trabalhos de criação e destruição de pilha a .gitlab-ci .yml.

Mais alguns trabalhos foram adicionados ao .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

No trecho de código acima, você pode ver que dois botões (deploy_staging, stop_staging) foram adicionados a Pipelines, exigindo ação manual.

Implantar aplicativos com o Docker Swarm
O nome da pilha corresponde ao nome da ramificação e essa exclusividade deve ser suficiente. Os serviços na pilha recebem endereços IP exclusivos e portas, diretórios, etc. será isolado, mas o mesmo de pilha para pilha (porque o arquivo de configuração é o mesmo para todas as pilhas) - o que queríamos. Implantamos a pilha (cluster) usando docker-compose.yml, que descreve nosso 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

Aqui você pode ver que os componentes estão conectados por uma rede (nw_swarm) e estão disponíveis uns para os outros.

Os componentes do sistema (baseados em redis, mysql) são separados do pool geral de componentes personalizados (nos planos e os personalizados são divididos como serviços). O estágio de implantação de nosso cluster parece passar o CMD para nossa grande imagem configurada e, em geral, praticamente não difere da implantação descrita na Parte I. Vou destacar as diferenças:

  • git clone... - obtenha os arquivos necessários para implantar (createconfig.py, install_venv.sh, etc.)
  • enrolar... && descompactar... - baixe e descompacte os artefatos de compilação (utilitários compilados)

Há apenas um problema ainda não descrito: os componentes que possuem uma interface da Web não podem ser acessados ​​nos navegadores dos desenvolvedores. Resolvemos esse problema usando proxy reverso, assim:

Em .gitlab-ci.yml, após o deploy da pilha do cluster, adicionamos a linha de deploy do balanceador (que, ao fazer commits, apenas atualiza sua configuração (cria novos arquivos de configuração nginx de acordo com o template: /etc/nginx/conf. d/${CI_COMMIT_REF_NAME}.conf) - consulte o código 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

Nos computadores de desenvolvimento, atualize /etc/hosts; prescrever url para nginx:

10.50.173.106 staging_BRANCH-1831_cluster.dev

Assim, a implantação de clusters de preparação isolados foi implementada e os desenvolvedores agora podem executá-los em qualquer número suficiente para verificar suas tarefas.

Planos futuros:

  • Separe nossos componentes como serviços
  • Tem para cada Dockerfile
  • Detecte automaticamente nós menos carregados na pilha
  • Especifique nós por padrão de nome (em vez de usar id como no artigo)
  • Adicione uma verificação de que a pilha foi destruída
  • ...

Agradecimentos especiais por статью.

Fonte: habr.com

Adicionar um comentário