Gitlab CI Docker PHP MySQL

21.01.2022

Есть проект на PHP (с composer), который предполагает командную разработку. После каждого коммита, и уж тем более после каждого merge request нужно знать что ничего не сломалось и иметь предположение что все работает, а также надо узнать процент покрытия кода. Все это будем делать при помощи Gitlab CI.

Gitlab CI достаточно простая и эффективная технология для организации CI/CD конвейера.

Gitlab CI состоит из:

  • инстанса Gitlab (собственно там где размещаются проекты с репозиториями)
  • проекта репозитория с файлом .gilab-ci.yml
  • Gitlab Runner - тот кто будет выполнять команды из .gilab-ci.yml
Gitlab Runner - это процессы выполняющие задания CI/CD. Они могут быть расположены как на одном сервере с инстансом Gitlab, так и на разных или вообще в Docker контейнере.

Ниже будет использован xdebug (модуль для php, который мы рассматривали здесь) и PHPUnit (о нем говорили здесь). Желательно ознакомится с ними чтобы было легче воспринимать картину в целом.

Подготовка среды конвейера

Заходим на машину где будет выполняться конвейер (в моем случае это специально выделенная для этого VPS) и начинаем ...

Устанавливаем Docker Eninge по документации, затем ставим Docker Compose, тоже по документации.

Теперь надо установить Gitlab Runner.

В инстансе Gitlab, заходим в проект, Settings=>CI/CD=>Runners здесь находится список всех раннеров доступных для проекта (но если читатель только начинает разбираться с Gitlab CI то этот список пуст):

  • Specific runners - раннеры специально созданные для этого проекта
  • Shared runners - раннеры в целом для инстанса Gitlab (если используется self hosted, а не gitlab.com)
Specific runners
Specific runners

А если читатель является админом инстанса Gitlab, то у него есть доступ в Admin=>Runners, где есть список всех доступных раннеров и возможность их настройки.

Администрирование всех раннеров
Администрирование всех раннеров

В разделе Specific runners так же есть токен для регистрации раннера, а при клике по кнопке Show Runner installation instructions получим инструкцию по установке и регистрации раннера на сервер (детальнее здесь):

•••
bash
# Download the binary for your system $ sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64 # Give it permissions to execute $ sudo chmod +x /usr/local/bin/gitlab-runner # Create a GitLab CI user $ sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash # Install and run as service $ sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner $ sudo gitlab-runner start

Теперь можно регистрировать раннер в инстансе Gitlab с сервера где установлен раннер таким образом:

•••
bash
$ sudo gitlab-runner register

В ходе регистрации будут запрошены необходимые данные типа адреса инстанса, токена и прочее. Также будет запрошен executor - средство исполнения команд:

•••
bash
Enter an executor: custom, docker-ssh, ssh, virtualbox, docker+machine, docker, parallels, shell, docker-ssh+machine, kubernetes

Выбираем shell так как исполнять команды будем через командную строку.

Теперь необходимо добавить пользователя gitlab-runner в группу docker чтобы он мог запускать docker:

•••
bash
$ sudo usermod -aG docker gitlab-runner

Структура проекта

Директория проекта содержит следующее:

  • src
  • tests
  • composer.json
  • phpunit.xml

.gitlab-ci.yml

Добавим файл .gitlab-ci.yml со следующим содержимым:

•••
yaml
# команды bash которые пойдут перед началом всех операций before_script: - docker -v - docker-compose -v # список стадий конвейера, объявляем только test stages: - test # секция стадии test test: stage: test # bash команды для выполнения - поднимаем комиозицию docker контейнеров script: - docker-compose -f docker-compose-test.yml up --abort-on-container-exit # задаем регулярное выражение для изъятия процента покрытия кода тестами, текст будет взят из вывода командной строки coverage: '/\s+Lines:\s{2,}(\d+[,.]\d+%)/' # указываем на каком раннере запускать tags: - mdk

Важно чтобы процесс завершил работу, иначе задание конвейера будет висеть! Сервис mysql по задумке работает вечно, но при помощи опции --abort-on-container-exit docker-compose начнет отключать сервис mysql после того как сервис php закончит свою работу.

Подробнее про структуру и ключевые слова можно прочитать в документации.

docker-compose

Выше мы использовали композицию контейнеров, которая должна быть описана в файле docker-compose-test.yml, создадим его в корне репозитория и заполним:

•••
yml
version: '3.0' # сервисы-контейнеры services: # сервис СУБД mysql: # используем image из репозитория docker image: mysql:5.7 # задаем имя контейнера container_name: mdk-mysql-test # передаем в окружение root пароль от БД environment: MYSQL_ROOT_PASSWORD: root # bash команда дял выполнения после старта контейнера command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci'] # подключаем в общую сеть networks: - app-network # сервис PHP php: # строим из Dockerfile и передаем внутрь аргументы из args build: context: ./.docker/php args: uid: 1000 user: runner # задаем имя контейнера container_name: mdk-php-test # стартуем только после того как стартует сервис mysql depends_on: - mysql # прокидываем внутрь контейнеров директории и файлы с хост машины volumes: - ./:/opt/mdk/ - ./.docker/php/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini - ./.docker/php/entrypoint.sh:/usr/bin/entrypoint.sh - ./.docker/php/wait-db.php:/opt/wait-db.php # bash команда для выполнения после старта контейнера command: /bin/bash -c "/usr/bin/entrypoint.sh" # подключаем в общую сеть networks: - app-network # конфигурация общей сети сервисов networks: app-network: driver: bridge

Подробнее о содержимом compose file можно прочитать в документации.

В services список контейнеров (на самом деле сервисов). При этом контейнеры могут быть взяты прямо из репозитория docker (как в случае mysql), либо сконфигурированы из Dockefile (как в случае с php).

Для обоих сервисов задаем одну общую сеть app-network, чтобы они могли общаться друг с другом по сетевым протоколам. Однако, для того чтобы из php можно было достучаться до mysql, в php надо использовать не localhost, а имя сервиса, в данном случае mysql.

Особый интерес у volumes - это прокидывание директорий и файлов с хост машины внутрь Docker контейнера, таким образом контейнер может работать с этими директориями и файлами так буд-то они принадлежат контейнеру. Все изменения на хост машине будут также применены и в Docker контейнере и наоборот.

Итак, у нас наметилась директория .docker/php/ создаем ее, ложим туда файлы:

xdebug.ini

Кратко мы уже говорили об этом в прошлый раз:

•••
ini
xdebug.remote_handler = dbgp xdebug.log=xdebug.log xdebug.mode=coverage,debug

Dockerfile

Dockerfile с конфигурацией контейнера php:

•••
yml
# создаем образ на основании cli php версии 7.3 FROM php:7.3-cli # объявялем что ждем аргументы ARG user ARG uid # обновляем список пакетов и устанавливаем что необходимо RUN apt update && apt install -y libzip-dev zip mc nano netcat # ставим модули php RUN docker-php-ext-install pdo_mysql RUN pecl install xdebug && docker-php-ext-enable xdebug # добавляем нового юзера, данные берем из аргументов, которые ожидаем из docker-compose-dev.yml RUN useradd -u $uid -m $user # ставим composer RUN cd ~ \ && curl -sS https://getcomposer.org/installer -o composer-setup.php \ && php composer-setup.php --install-dir=/usr/local/bin --filename=composer # исполняем скрипты от нового юзера USER $uid # задаем рабочую директорию WORKDIR /opt/mdk/
По умолчанию все будет исполняться от root и на хост машине надо будет удалять от root. Но если поменять юзера (исполняющего команды и создающего новые файлы) таким же простым как на хост машине, тогда не надо будет переключаться на root при измении этих файлов. Немного здесь почитать.

wait-db.php

•••
php
<?php /* ожидание соединения с mysql */ $pdo = null; while (!$pdo) { try { $pdo = new PDO('mysql:host=mysql', 'root', 'root'); echo "MySQL is available\n"; } catch (Exception $e) { echo "MySQL is unavailable - sleeping\n"; $pdo = null; sleep(1); } }

entrypoint.sh

•••
bash
#!/bin/bash # установка пакетов php composer install # запуск unit тестов vendor/bin/phpunit --colors=always --coverage-text --bootstrap tests/Unit/bootstrap.php tests/Unit/ # ожидание соединения с БД php -f /opt/wait-db.php # запуск системных тестов vendor/bin/phpunit --colors=always --bootstrap tests/System/bootstrap.php tests/System/

Ключ depends_on сообщает сервису что он должен стартовать только после того как стартует сервис указанный в ключе (mysql), но это не значит что сервис, которого ждут, будет полностью инициализирован. Для этого в зависимом контейнере нужно самостоятельно определить доступность нужного сервиса (в данном случае это делает wait-db.php).

Проверка

Теперь когда у нас все настроено, можно пушить в репозиторий и наблюдать работу конвейера. Заходим на страницу проекта затем CI/CD=>Jobs здесь список всех заданий. В столбце Coverage указан процент покрытия кода тестами :)

Список всех заданий (pipelines)
Список всех заданий (pipelines)
Выполнение задания (pipeline)
Выполнение задания (pipeline)

Если есть какие-то ошибки - исправляем. Первая ошибка была Job failed (system failure): preparing environment.

В ходе выполнения я сделал отдельную ветку, на которой производил тесты настройки конвейера, пушей было несколько десятков.

После успешного прохождения заданий можно прикрепить красивые бейджики в README репозитория, их можно найти в Settings=>CI/CD=>Gtntral pipelines.

Бейдж coverage report
Бейдж coverage report

Итог

Получилось объемно, но на самом деле все просто :)

В дополнение рекомендую почитать по теме здесь, там, тут и еще вот.