Тестовый сервер для команды разработчиков

от автора

Привет хабр! В данной статье я хочу поделиться опытом разворачивания тестового сервера для команды разработчиков. Вкратце суть проблемы — есть команда разработки и несколько проектов на php. Пока нас было мало и проект был по сути один, то использовался 1 тестовый сервер и чтобы показать задачу заказчику — разработчик «столбил» сервер на определенное время. Если «окон» по времени не было, то приходилось ждать. Со временем рос коллектив и сложность задач, соответственно увеличивалось время проверки и занятость тестового сервера, что негативно влияло на сроки выполнения и премию. Поэтому пришлось искать решение и оно под катом.

Вводная

Что было:
1. Один тестовый сервер
2. Gitlab и redmine на другом сервере
3. Желание разобраться в проблеме
Все сервера находятся в нашей локальной сети, тестовый сервер недоступен извне.

Что требовалось:
1. Возможность тестировать несколько проектов/веток одновременно
2. Разработчик может зайти на сервер, до настроить его и при этом не сломать ничего у других
3. Все должно быть максимально удобно и делаться по 1 кнопке желательно из gitlab (CI/CD).

Варианты решений

1. Один сервер, много хостов

Самый простой вариант. Используем тот же тестовый сервер, только разработчику нужно создавать хост под каждую ветку/проект и вносить его в конфигурацию nginx/apache2.
Плюсы:

  1. Быстро и всем понятно
  2. Можно автоматизировать

Минусы:

  1. Не выполняется п.2 из требований — разработчик может запустить обновление бд и при некотором стечении обстоятельств положить все (Привет Андрей!)
  2. Довольно сложная автоматизация с кучей конфигурационных файлов

2. Каждому разработчику по серверу!

Выделяем каждому по серверу и разработчик сам отвечает за свое хозяйство.
Плюсы:

  1. Разработчик может полностью настроить сервер под свой проект

Минусы:

  1. п.2 требований так и не выполняется
  2. Дорого и ресурсы могут просто простаивать пока идет разработка, а не тестирование
  3. Автоматизация еще сложней чем в п.1 из-за разных серверов

3. Контейнеризация — docker, kubernetes

Данная технология все больше проникает в нашу жизнь. Дома я уже давно использую для своих проектов docker.

Docker — программное обеспечение для автоматизации развёртывания и управления приложениями в среде виртуализации на уровне операционной системы. Позволяет «упаковать» приложение со всем его окружением и зависимостями в контейнер, который может быть перенесён на любую Linux-систему с поддержкой cgroups в ядре, а также предоставляет среду по управлению контейнерами.

Плюсы:

  1. Используется один сервер
  2. Выполняются все пункты требований

Минусы:

  1. Образы и контейнеры порой отнимают довольно много места, приходится кроном чистить уже устаревшие для освобождения места.

Внедрение docker

При использовании gitlab очень часто попадались на глаза настройки AutoDevOps, kubernetes. Плюс бородатые дядьки на различных meetup рассказывают как у них круто все работает с kubernetes. Поэтому было принято решение попробовать развернуть кластер на своих мощностях, был выпрошен сервер (а тестовый трогать нельзя, там люди тестируют) и понеслась!

Так как опыта у меня с kubernetes 0, делось все по мануалу с попыткой понять как все эти кластера работают. Спустя некоторое время мне удалось поднять кластер, но потом пошли проблемы с сертификатами, ключами, да и вообще с трудностью развертывания. Мне же нужно было решение проще, чтобы научить своих коллег с этим работать (например, тот же отпуск не хочется проводить сидящим в скайпе и помогающим с настройкой). Поэтому kubernetes был оставлен в покое. Оставался сам docker и нужно было найти решение для маршрутизации контейнеров. Так как их можно было поднять на разных портах, то можно было бы использовать тот же nginx для внутреннего перенаправления. Называется это обратный прокси сервер.

Обратный прокси-сервер — тип прокси-сервера, который ретранслирует запросы клиентов из внешней сети на один или несколько серверов, логически расположенных во внутренней сети. При этом для клиента это выглядит так, будто запрашиваемые ресурсы находятся непосредственно на прокси-сервере.

Обратный прокси-сервер

Чтобы не изобретать велосипед, я начал искать готовые решения. И оно нашлось — это traefik.
Træfik — это современный обратный прокси HTTP и балансировщик нагрузки, который упрощает развертывание микросервисов. Træfik интегрируется с существующими инфраструктурными компонентами ( Docker, Swarm mode, Kubernetes, Marathon, Consul, Etcd, Rancher, Amazon ECS, …) и настраивается автоматически и динамически. Для работы с docker достаточно указать его сокет и все, дальше Træfik сам находит все контейнеры и маршрутизацию до них (подробнее в «Упаковываем приложения в docker»).

Конфигурация контейнера Træfik

Запускаю его через docker-compose.yml

version: '3'  services:   traefik:     image: traefik:latest # The official Traefik docker image     command: --api --docker # Enables the web UI and tells Træfik to listen to docker     ports:       - 443:443       - 80:80     # The HTTP port       - 8080:8080 # The Web UI (enabled by --api)     volumes:       - /var/run/docker.sock:/var/run/docker.sock # So that Traefik can listen to the Docker events       - /opt/traefik/traefik.toml:/traefik.toml       - /opt/traefik/certs/:/certs/     networks:     - proxy     container_name: traefik     restart: always networks:   proxy:     external: true 

Здесь мы сообщаем прокси, что нужно слушать порты 80,443 и 8080 (веб морда прокси), монтируем сокет докера, файл конфигурации и папку с сертификатами. Для удобства именования тестовых сайтов, мы решили сделать локальную доменную зону *.test. При обращении к любому сайту на ней, пользователь попадает на наш тестовый сервер. Поэтому сертификаты в папке traefik самоподписаные, но он так поддерживает Let’s Encrypt.
Генерация сертификатов

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout domain.key -out domain.crt

Перед стартом нужно создать в докере сеть proxy (можете назвать по своему).

docker network create proxy

Это будет сеть для связи traefik с контейнерами php сайтов. Поэтому указываем ее в параметре networks сервиса и в networks всего файла указав в параметре external: true.

Файл traefik.toml

debug = false  logLevel = "DEBUG" defaultEntryPoints = ["https","http"] #точки входа insecureSkipVerify = true  #принимать самоподписаные сертификаты  [entryPoints]   [entryPoints.http]   address = ":80"   [entryPoints.https]   address = ":443"   [entryPoints.https.tls]  [docker] endpoint = "unix:///var/run/docker.sock" domain = "docker.localhost" watch = true exposedbydefault = false

Тут все довольно просто — указываем точки входа http и https трафика, не забудьте поставить insecureSkipVerify = true если сертификаты локальные. В секции entryPoints.https.tls можно не указывать сертификаты, тогда traefik подставит свой сертификат.
Можно запустить сервис

docker-compose up -d

Если перейти по адресу site.test, то выдаст ошибку 404, так как этот домен не привязан ни к какому контейнеру.

Упаковываем приложения в docker

Теперь нужно настроить контейнер с приложением, а именно:
1. указать в сетях сеть proxy
2. добавить labels с конфигурацией traefik

Ниже приведена конфигурация одного из приложений

docker-compose.yml приложения

version: '3' services:   app:     build: data/docker/php    #кастомная сборка сервера     restart: always     working_dir: /var/www/html/public     volumes:     - ./:/var/www/html   #монтирование папки с сайтом     - /home/develop/site-files/f:/var/www/html/public/f #монтирование папки с загрузками для экономии места     links:     - mailcatcher     - memcached     - mysql     labels:     - traefik.enabled=true     - traefik.frontend.rule=Host:TEST_DOMAIN,crm.TEST_DOMAIN,bonus.TEST_DOMAIN     - traefik.docker.network=proxy     - traefik.port=443     - traefik.protocol=https     networks:     - proxy     - default    mailcatcher:     image: schickling/mailcatcher:latest     restart: always    memcached:     image: memcached     restart: always    mysql:     image: mysql:5.7     restart: always     command: --max_allowed_packet=902505856 --sql-mode=""     environment:       MYSQL_ROOT_PASSWORD: 12345       MYSQL_DATABASE: site     volumes:     - ./data/cache/mysql-db:/var/lib/mysql # сохранение файлов БД на хосте    phpmyadmin:     image: phpmyadmin/phpmyadmin     restart: always     links:     - mysql     environment:       MYSQL_USERNAME: root       MYSQL_ROOT_PASSWORD: 12345       PMA_ARBITRARY: 1       PMA_HOST: mysql_1     labels:     - traefik.enabled=true     - traefik.frontend.rule=Host:pma.TEST_DOMAIN     - traefik.docker.network=proxy     - traefik.port=80     - traefik.default.protocol=http     networks:     - proxy     - default networks:   proxy:     external: true 

В сервисе app, в секции сети нужно указать proxy и default, это значит что он будет доступен в двух сетях, как видно из конфигурации я не пробрасываю порты наружу, все идет внутри сети.
Далее конфигурируем labels

    - traefik.enabled=true   #включение traefik для данного сервиса     - traefik.frontend.rule=Host:TEST_DOMAIN,crm.TEST_DOMAIN,bonus.TEST_DOMAIN #перечисление доменов для которых traefik будет перенаправлять запросы сюда     - traefik.docker.network=proxy   #сеть для связи     - traefik.port=443                      #порт, если у вас нет ssl то укажите 80 и ниже http     - traefik.protocol=https    #используемый протокол      #в секции phpmyadmin приведен пример http подключения 

В общей секции networks нужно указать external: true
Константу TEST_DOMAIN нужно заменить на домен, например, site.test
Запускаем приложение

docker-compose up -d

Теперь если зайти на домены site.test, crm.site.test, bonus.site.test можно увидеть рабочий сайт. А на домене pma.site.test будет phpmyadmin для удобной работы с бд.

Настройка GitLab

Создаем обработчик заданий, для этого запускаем

gitlab-runner register

Указываем url gitlab, токен и через что будет выполняться задание (executors). Так как у меня тестовый и gitlab находятся на разных серверах, то выбираю ssh executor. Нужно будет указать адрес сервера и логин/пароль для подключения по ssh.
Runner можно сделать прикрепленным к одному или нескольким проектам. Так как у меня логика работы везде одинаковая, поэтому был создан shared runner (общий для всех проектов).
И последний штрих это создать файл конфигурации CI

.gitlab-ci.yml

stages: - build - clear  #Конфигурация для develop build_develop:   stage: build    #относим к этапу build   tags:               #если нужно можно указать теги   - ssh-develop   environment:   #настройки окружения, после разворачивания они выведутся в Операции - Среды проекта     name: review/$CI_BUILD_REF_NAME  #название проекта     url: https://site$CI_PIPELINE_ID.test  #url для доступа к нему     on_stop: clear   when: manual   script:   - cd ../ && cp -r $CI_PROJECT_NAME $CI_PIPELINE_ID && cd $CI_PIPELINE_ID  #копирование проекта в отдельную папку   - cp -r /home/develop/site-files/.ssh  data/docker/php/.ssh  #ключи для ssh   - sed -i -e "s/TEST_DOMAIN/site$CI_PIPELINE_ID.test/g" docker-compose.yml    #Замена имени домена   - docker-compose down  #на случай ребилда   - docker-compose up -d --build  #билд образов   - script -q -c "docker exec -it ${CI_PIPELINE_ID}_app_1 bash -c \"cd ../ && php composer.phar install --prefer-dist \"" #установка пакетов компосера   - script -q -c "docker exec -it ${CI_PIPELINE_ID}_app_1 bash -c \"cd ../ && php composer.phar first-install $CI_PIPELINE_ID\"" #запуск скрипта первичной настройки приложения  #конфигурация для production build_prod:   stage: build   tags:   - ssh-develop   environment:     name: review/$CI_BUILD_REF_NAME     url: https://site$CI_PIPELINE_ID.test     on_stop: clear   when: manual   script:   - cd ../ && cp -r $CI_PROJECT_NAME $CI_PIPELINE_ID && cd $CI_PIPELINE_ID   - cp -r /home/develop/site-files/.ssh  data/docker/php/.ssh  #ключи для ssh   - docker-compose down   - docker-compose up -d --build   - script -q -c "docker exec -it ${CI_PIPELINE_ID}_app_1 bash -c \"cd ../ && php composer.phar install --prefer-dist --no-dev\""   - script -q -c "docker exec -it ${CI_PIPELINE_ID}_app_1 bash -c \"cd ../ && php composer.phar first-install $CI_PIPELINE_ID\""  clear:   stage: clear   tags:   - ssh-develop   environment:     name: review/$CI_BUILD_REF_NAME     action: stop   script:   - cd ../ && cd $CI_PIPELINE_ID && docker-compose down && cd ../ && echo password | sudo -S rm -rf $CI_PIPELINE_ID  #Остановка контейнеров и удаление папки с проектом   when: manual 

В данной конфигурации описаны 2 этапа — build и clear. Этап build имеет 2 варианта выполнения — build_develop и build_prod

Gitlab строит понятную диаграмму выполнения процесса. В моем примере все процессы стартуют вручную (параметр when: manual). Сделано это для того, чтобы разработчик после разворачивания тестового сайта, мог делать pull своих правок в контейнер без пересборки всего контейнера. Еще одна причина это наименование доменов — site$CI_PIPELINE_ID.test, где CI_PIPELINE_ID — номер процесса запустившего сборку. То есть отдали на проверку сайт с доменом site123.test и чтобы внести горячие правки, сразу заливаются изменения в контейнер самим разработчиком.
Небольшая особенность работы ssh executor. При подключении к серверу создается папка вида

/home/пользователь/builds/хеш_runner/0/Группа_проекта/Название_проекта

Поэтому была добавлена строчка

cd ../ && cp -r $CI_PROJECT_NAME $CI_PIPELINE_ID && cd $CI_PIPELINE_ID

В ней мы поднимаемся на папку выше и копируем проект в папку с номером процесса. Так можно разворачивать несколько веток одного проекта. Но в настройках обработчика нужно поставить галку Lock to current projects, так обработчик не будет пытаться развернуть несколько веток одновременно.
Этап clear останавливает контейнеры и удаляет папку, могут понадобиться права root, поэтому используем команду echo password | sudo -S rm, где password ваш пароль.

Уборка мусора

Время от времени нужно удалять не используемые контейнеры, чтобы не занимать место, для этого в кроне висит скрипт с таким содержанием

#!/bin/bash # удаление завершенных контейнеров: docker ps --filter status=dead --filter status=exited -aq | xargs -r docker rm -v # удаление неиспользуемых контейнеров: yes | docker container prune # удаление не используемых образов: yes | docker image prune # удаление не используемых томов: yes | docker volume prune

выполняется раз в день.

Заключение

Данное решение помогло нам существенно оптимизировать тестирование и выпуск новых фич. Готов ответить на вопросы, конструктивная критика принимается.

Бонус

Для того чтобы не собирать каждый раз образы из Dockerfile, можно хранить их локальном реестре докера.

Файл docker-compose.yml

registry:   restart: always   image: registry:2   ports:     - 5000:5000   volumes:     - /opt/docker-registry/data:/var/lib/registry #монтирование папки для хранения образов 

В данном варианте не используется аутентификация, это не безопасный способ (!!!), но для хранения не критичных образов нам подходит.
Можно настроить gitlab для просмотра

 gitlab_rails['registry_enabled'] = true  gitlab_rails['registry_host'] = "registry.test"  gitlab_rails['registry_port'] = "5000" 

После этого в gitlab появляется список образов


ссылка на оригинал статьи https://habr.com/post/426319/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *