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:
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:
- 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, ...)
- instalar_venv.sh - cria um ambiente virtual para componentes py em um diretório específico e o copia para servidores remotos
- prepare_init.d.py — prepara scripts start-stop para o componente com base no modelo
- 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
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
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.
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
- ...