Привет, Хабр! В данной статье я хочу поделиться опытом разворачивания тестового сервера для команды разработчиков. Вкратце суть проблемы — есть команда разработки и несколько проектов на php. Пока нас было мало и проект был по сути один, то использовался 1 тестовый сервер и чтобы показать задачу заказчику — разработчик «столбил» сервер на определенное время. Если «окон» по времени не было, то приходилось ждать. Со временем рос коллектив и сложность задач, соответственно увеличивалось время проверки и занятость тестового сервера, что негативно влияло на сроки выполнения и премию. Поэтому пришлось искать решение и оно под катом.
Что было:
Все сервера находятся в нашей локальной сети, тестовый сервер недоступен извне.
Что требовалось:
Самый простой вариант. Используем тот же тестовый сервер, только разработчику нужно создавать хост под каждую ветку/проект и вносить его в конфигурацию nginx/apache2.
Плюсы:
Минусы:
Выделяем каждому по серверу и разработчик сам отвечает за свое хозяйство.
Плюсы:
Минусы:
Данная технология все больше проникает в нашу жизнь. Дома я уже давно использую для своих проектов 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»).
Здесь мы сообщаем прокси, что нужно слушать порты 80,443 и 8080 (веб морда прокси), монтируем сокет докера, файл конфигурации и папку с сертификатами. Для удобства именования тестовых сайтов, мы решили сделать локальную доменную зону *.test. При обращении к любому сайту на ней, пользователь попадает на наш тестовый сервер. Поэтому сертификаты в папке traefik самоподписаные, но он так поддерживает Let's Encrypt.
Генерация сертификатов
Перед стартом нужно создать в докере сеть proxy (можете назвать по своему).
Это будет сеть для связи traefik с контейнерами php сайтов. Поэтому указываем ее в параметре networks сервиса и в networks всего файла указав в параметре external: true.
Тут все довольно просто — указываем точки входа http и https трафика, не забудьте поставить insecureSkipVerify = true если сертификаты локальные. В секции entryPoints.https.tls можно не указывать сертификаты, тогда traefik подставит свой сертификат.
Можно запустить сервис
Если перейти по адресу site.test, то выдаст ошибку 404, так как этот домен не привязан ни к какому контейнеру.
Теперь нужно настроить контейнер с приложением, а именно:
1. указать в сетях сеть proxy
2. добавить labels с конфигурацией traefik
Ниже приведена конфигурация одного из приложений
В сервисе app, в секции сети нужно указать proxy и default, это значит что он будет доступен в двух сетях, как видно из конфигурации я не пробрасываю порты наружу, все идет внутри сети.
Далее конфигурируем labels
В общей секции networks нужно указать external: true
Константу TEST_DOMAIN нужно заменить на домен, например, site.test
Запускаем приложение
Теперь если зайти на домены site.test, crm.site.test, bonus.site.test можно увидеть рабочий сайт. А на домене pma.site.test будет phpmyadmin для удобной работы с бд.
Создаем обработчик заданий, для этого запускаем
Указываем url gitlab, токен и через что будет выполняться задание (executors). Так как у меня тестовый и gitlab находятся на разных серверах, то выбираю ssh executor. Нужно будет указать адрес сервера и логин/пароль для подключения по ssh.
Runner можно сделать прикрепленным к одному или нескольким проектам. Так как у меня логика работы везде одинаковая, поэтому был создан shared runner (общий для всех проектов).
И последний штрих это создать файл конфигурации CI
В данной конфигурации описаны 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. При подключении к серверу создается папка вида
Поэтому была добавлена строчка
В ней мы поднимаемся на папку выше и копируем проект в папку с номером процесса. Так можно разворачивать несколько веток одного проекта. Но в настройках обработчика нужно поставить галку Lock to current projects, так обработчик не будет пытаться развернуть несколько веток одновременно.
Этап clear останавливает контейнеры и удаляет папку, могут понадобиться права root, поэтому используем команду echo password | sudo -S rm, где password ваш пароль.
Время от времени нужно удалять не используемые контейнеры, чтобы не занимать место, для этого в кроне висит скрипт с таким содержанием
выполняется раз в день.
Данное решение помогло нам существенно оптимизировать тестирование и выпуск новых фич. Готов ответить на вопросы, конструктивная критика принимается.
Для того чтобы не собирать каждый раз образы из Dockerfile, можно хранить их локальном реестре докера.
В данном варианте не используется аутентификация, это не безопасный способ (!!!), но для хранения не критичных образов нам подходит.
Можно настроить gitlab для просмотра
После этого в gitlab появляется список образов
Вводная
Что было:
- Один тестовый сервер
- Gitlab и redmine на другом сервере
- Желание разобраться в проблеме
Все сервера находятся в нашей локальной сети, тестовый сервер недоступен извне.
Что требовалось:
- Возможность тестировать несколько проектов/веток одновременно
- Разработчик может зайти на сервер, до настроить его и при этом не сломать ничего у других
- Все должно быть максимально удобно и делаться по 1 кнопке желательно из gitlab (CI/CD).
Варианты решений
1. Один сервер, много хостов
Самый простой вариант. Используем тот же тестовый сервер, только разработчику нужно создавать хост под каждую ветку/проект и вносить его в конфигурацию nginx/apache2.
Плюсы:
- Быстро и всем понятно
- Можно автоматизировать
Минусы:
- Не выполняется п.2 из требований — разработчик может запустить обновление бд и при некотором стечении обстоятельств положить все (Привет Андрей!)
- Довольно сложная автоматизация с кучей конфигурационных файлов
2. Каждому разработчику по серверу!
Выделяем каждому по серверу и разработчик сам отвечает за свое хозяйство.
Плюсы:
- Разработчик может полностью настроить сервер под свой проект
Минусы:
- п.2 требований так и не выполняется
- Дорого и ресурсы могут просто простаивать пока идет разработка, а не тестирование
- Автоматизация еще сложней чем в п.1 из-за разных серверов
3. Контейнеризация — docker, kubernetes
Данная технология все больше проникает в нашу жизнь. Дома я уже давно использую для своих проектов docker.
Docker — программное обеспечение для автоматизации развёртывания и управления приложениями в среде виртуализации на уровне операционной системы. Позволяет «упаковать» приложение со всем его окружением и зависимостями в контейнер, который может быть перенесён на любую Linux-систему с поддержкой cgroups в ядре, а также предоставляет среду по управлению контейнерами.Плюсы:
- Используется один сервер
- Выполняются все пункты требований
Минусы:
- Образы и контейнеры порой отнимают довольно много места, приходится кроном чистить уже устаревшие для освобождения места.
Внедрение 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 появляется список образов