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

    Привет, Хабр! В данной статье я хочу поделиться опытом разворачивания тестового сервера для команды разработчиков. Вкратце суть проблемы — есть команда разработки и несколько проектов на 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 появляется список образов

    Share post

    Comments 10

      0
      echo password | sudo -S rm,

      Решая подобную задачу, меня напрягло решение запускать Ранер с правами рута или давать ему их через sudo. Так-же возня с копированием проекта — работает пока проект мал.
      Для себя выбрал использование: dind (Docker in Docker) — gitlab-ci поддерживает его.

      Заметил в скрипте ci:
      - script -q -c "docker e.....
      для чего так сделано? именно: script -q -c?
        0
        У меня подключается через обычного пользователя, но повышение прав нужно для удаление некоторых файлов из проекта, которые появляются в процессе работы (не во всех проектах нужно sudo). Какие варианты есть если не копирование, чтобы сделать независимую папку с проектом?
        Пробовал dind меня смутил как раз таки фактор запуска контейнера внутри контейнера, плюс запуск в привилегированном режиме.
        script -q -c нужен для того, чтобы не было ошибки «the input device is not a TTY»
          0
          script -q -c нужен для того, чтобы не было ошибки «the input device is not a TTY»

          убрать параметр -it, для ci он не нужен
        0
        Хотелось бы уточнит, кажется я не до конца понял:

        1. Что вы тестируете? Каждую задачу/ветку по отдельности или смерживаете все ветки и тестируете сразу кучу задач?

        2. Если каждую задачу/ветку, то как сделано, чтобы они не мешали друг-другу? Какие будут домены у этих двух задач?

        3. Кто останавливает контейнеры задач, которые уже протестированы?
          0
          1. Каждая ветка по отдельности, у нас теперь фичи выходят по мере готовности, а не в 1 день кучей. Потом бывает тяжело найти причину если что-то сломается и заказчикам не надо ждать дня релиза.
          2. Для этого все и делалось. Внутри нашей локальной сети админы прописали доменную зону *.test ведущий на тестовый сервер. Все контейнеры маршрутизируются через Træfik, об этом подробно написано в статье. У нас домены именуются так site512.test где 512 это номер процесса сборки в гитлабе.
          3. Контейнеры останавливает сам разработчик, если фича не вышла, либо будет еще несколько выкатываний на тестовый. Так как мы используем гитлаб, то при закрытии запроса на слияние, окружение останавливается автоматически гитлабом.
            0
            > У нас домены именуются так site512.test где 512 это номер процесса сборки в гитлабе.

            Т.е. если в ветку был сделан еще один коммит (ну, опечатку поправили), то у развернутой ветки меняется домен? Пайплайн же новый будет. Как это контролируется? Как заказчику или тестировщику угадать, какой сейчас домен актуален для этой задачи?
              0
              У нас поднят Portainer, разработчик может свободно зайти в консоль контейнера и сделать пул, либо донастроить контейнер. Для больших правок, контейнер останавливается и создается новый. Автоматом контейнеры не создаются, только запуск тестов
              image
              Дальше разработчик выбирает в каком окружении запускать и только в этот момент создается сайт. В гитлабе есть у проекта пункты меню Операции — Среды. Там отображается последняя развернутая версия ссылкой на нее. Этот адрес и отдается заказчику/тестировщику. Сейчас тестировщиков приучаем заходить туда самим.
              image
              0
              3. Контейнеры останавливает сам разработчик

              Что если фича еще не принята, но и не отменена, заказчик «думает»? Сколько задач одновременно может быть запущено?
                0
                Да сколько угодно, пока ресурсов сервера хватает. У нас крутится 5-15 контейнеров с сайтами. Проблем нет.
                Единственное я примерно раз в 2 недели захожу в Portainer и смотрю нет ли старых сборок, если разраб забыл сам остановить. Контролируется опять же это по номерам pipeline.
            0
            del

            Only users with full accounts can post comments. Log in, please.