Yii2 приложение в docker +codeception в gitlab pipelines
Встала передо мною сегодня такая задача. как запуск одного из сайтов. находящихся у меня на поддержке, в докере при помощи docker-compose. Запустить это Yii приложение в докере я его хочу потому, что сейчас у меня плавно идет миграция всех проектов на систему работы с docker-compose и ci от гитлаба. А у проекта, о котором сегодня речь, возникла необходимость обмена файлами с 1С сервером. Предоставить ограниченный доступ sftp серверу только до одной папки проекта - это же идеальный вариант использовать для этого докер. В общем, пакуя сегодня этот проект в докер и перенося его на другой сервер я убиваю трех зайцев: настраиваю sftp сервер для обмена данными с 1С, переношу этот проект на другой сервер и начинаю его деплоить и тестировать через ci гитлаба, и заодно пишу эту статью.
Сейчас этот проект висит в проде по стандартной олд-скул схеме: общий для пары десятков сайтов сервер mysql, установленный на хостовой машине, + пхп как модуль для апача + нгинкс. Такой вот ретро-подход без fpm.
Для того, чтобы обеспечить нормальную работу проекта с использованием docker-compose и выгрузкой его на прод нам необходимо будет сделать несколько несложных вещей:
- составить docker-compose файл, причем в git репозиторий я предлагаю добавить две версии файла: dev (она же будет использоваться в тестах) и prod-версию;
- поправить некоторые файлы проекта для получения параметров работы приложения из переменных окружения контейнера;
- составить .gitlab-ci.yml файл для запуска тестов и публикации приложения;
- протестировать работу gitlab runner на локальной машине (в моем случае - мак).
Запуск в docker
Для запуска проекта в минимальной конфигурации в докере нам потребуется использовать 3 контейнера: nginx, mysql и php-fpm. Для запуска acceptance тестирования в браузере нам понадобится также контейнер с сервером selenium и chrom (в нашем случае ограничимся прогону приемочных тестов в хроме). В некоторых других случаях, помимо трех основных контейнеров, могут еще понадобится контейнейры sphinx, memcache или redis - их можно добавить в docker-compose и настроить в конфиге приложения по аналогии с mysql.
Итак, в моем случае dev-версия конфига для basic приложения будет выглядеть так:
version: '2'
services:
selenium:
image: selenium/standalone-chrome
mysql:
ports:
- '127.0.0.1:3305:3306'
image: mysql:5.7
volumes:
- ./docker/mysql/data:/var/lib/mysql
- ./docker/mysql/config/my.cnf:/etc/mysql/my.cnf
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: database
MYSQL_USER: yii2
MYSQL_PASSWORD: database
nginx:
image: nginx
volumes:
- ./:/app:delegated
- ./docker/nginx/:/etc/nginx/conf.d/
ports:
- '8888:80'
php:
build:
context: ./
dockerfile: Dockerfile
environment:
YII_DEBUG: 'true'
YII_ENV: 'dev'
YII_MAIL_FILE_TRANSPORT: 'true'
volumes:
- ./docker/php/php.ini:/usr/local/etc/php/conf.d/php.ini
- ./:/app:delegated
Несколько пояснений по поводу конфига:
Порт mysql мы выставляем наружу на локальную машину на 3305 порт для того, чтобы во время разработки на локальной машине иметь возможность подключить к базе данных приложения phpstorm или какой-то другой клиент.
Для контейнера mysql пробрасываем на улицу папку для хранения файлов базы, а также конфиг my.conf для того, чтобы иметь возможность хранить конфигурацию сервера базы данных прямо в репозитории нашего приложения.
В секции контейнера nginx мы экспозим 80й порт контейнера на 8888 порт локальной машины, причем, хочу обратить внимание, что в случае с версией docker-compose файла для разработки, а не продакшена, экспоузить лучше не указывая локальный хост “127.0.0.1:8888:80”, а вешать на все интерфейсы “8888:80”, включая публичный, чтобы иметь возможность проверить работу сайта с телефонов и других устройств локальной сети, введя ip и порт нашей машины.
В секции контейнера php мы указываем несколько переменных окружения, которые мы будем слушать в приложении. Кроме того, мы не указываем имя образа php:7.3-fpm, а собираемся собирать свой образ на его основе из Dockerfile. Это потому, что стандартный образ php-fpm не содержит огромного количества дополнительных php модулей и debian пакетов, которые необходимы для работы и нашего приложения, и даже стандартного yii2-basic-app. Доставлять к стандартному образу мы будем следующие модули php:
- gd (причем обязательно с поддержкой webp - так как мой файловый модуль конвертирует картинки в этот формат)
- imap
- zip
- intl
- pdo_mysql
- xdebug (который по дефолту будет выключен)
А также в доставляем composer.phar и zip, git, и ffmpeg для кодировки видео-файлов (мой модуль для работы с файлами для yii2 умеет конвертировать видео-файлы в mp4 приемлемого разрешения). В итоге Dockerfile выглядит как-то так:
FROM php:7.3-fpm
RUN apt-get update
RUN apt-get install -y \
git \
libzip-dev \
libc-client-dev \
libkrb5-dev \
libpng-dev \
libjpeg-dev \
libwebp-dev \
libfreetype6-dev \
libkrb5-dev \
libicu-dev \
zlib1g-dev \
zip \
ffmpeg
RUN docker-php-ext-configure gd \
--with-webp-dir=/usr/include/ \
--with-freetype-dir=/usr/include/ \
--with-jpeg-dir=/usr/include/
RUN docker-php-ext-install gd
RUN docker-php-ext-configure imap \
--with-kerberos \
--with-imap-ssl
RUN docker-php-ext-install imap
RUN docker-php-ext-configure zip \
--with-libzip
RUN docker-php-ext-install zip
RUN docker-php-ext-configure intl
RUN docker-php-ext-install intl
RUN docker-php-ext-install pdo
RUN docker-php-ext-install pdo_mysql
RUN docker-php-ext-install exif
RUN pecl install xdebug
RUN docker-php-ext-enable xdebug
RUN echo "#zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20180731/xdebug.so" > /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
RUN curl --silent --show-error https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
WORKDIR /app
Теперь в корне проекта создадим папку docker со следующей структурой, для хранения конфигов и данных контейнеров:
- docker
- php
- php.ini
- mysql
- conf
- my.conf
- data
- conf
- nginx
- app.conf
- .gitignore
- php
Если вам не надо никак менять конфигурацию mysql, то my.conf можно оставить пустым на данном этапе. Конфиг app.conf для nginx у меня выглядит так:
server {
listen 80;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /app/web/;
index index.php index.html;
client_max_body_size 5000m;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}
Собственно тут все просто: биндимся на 80 порт, увеличиваем размер возможного загружаемого файла до 5гб, для всех запросов кроме .php отправляем всех в папку проекта, для остальных - отправляем на хост php на 9000 порт где у нас висит контейнер c php-fpm.
В php.ini добавляем несколько необходимых нам параметров:
upload_max_filesize = 5000M
post_max_size = 5000M
date.timezone = Europe/Moscow
Теперь у нас все готово для того, чтобы собрать наш контейнер с php и затем запустить всю группу.
Добавляем наши новые файлы и папки в репозиторий и запускаем сборку, а по ее окончанию - запускаем группу.
$ git add docker-compose.dev.yml
$ git add docker
$ git add Dockerfile
$ cp docker-compose.dev.yml docker-compose.yml
$ docker-compose build
$ docker-compose up -d
Все должно запуститься. Проверить запущенные контейнеры можно командой $docker-compose ps, вывод которой покажет список контейнеров группы а также их проброшенные папки и порты.
Если пакеты composer еще не установлены в папке vendor, то можно выполнить команду, которая запустить их установку внутри контейнера (я предпочитаю с ключом -vv для более подробного вывода):
$ docker-compose exec php composer install -vv
Теперь, если мы обратимся на локальный хост и порт 127.0.0.1:8888 мы увидим ошибку фреймворка Yii2 о Database Exception - Connection Error, ведь мы еще не отредактировали код нашего приложения для работы с докером.
Конфиг базы данных у нас хранится в /config/db.php и шарится между web и консольным приложением. У меня он не был добавлен в репозиторий, а копировался с /config/db.example.php и правился под конкретный случай (разработка или прод). Теперь можно добавить /config/db.php, указав там постоянные параметры доступа к базе докера, а /config/db.example.php удалить. При этом, активировать enableSchemaCache для случая, когда система на продакшене. По итогу, /config/db.php выглядит так:
<?php
return [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=mysql;dbname=database',
'username' => 'yii2',
'password' => 'database',
'charset' => 'utf8',
'enableSchemaCache' => YII_ENV == 'prod',
'schemaCacheDuration' => 6000,
'schemaCache' => 'cache',
];
Еще в секции конфига components[‘mailer’] приводим все вот к такому виду, давая возможность сконфигурировать параметры сервера для отправки почты, а также активировать file tranport для тестирования и разработки с помощью переменных окружения, которые мы зададим для этого контейнера в docker-compose.yml файле.
'mailer' => [
'class' => 'yii\swiftmailer\Mailer',
'useFileTransport' => getenv('YII_MAIL_FILE_TRANSPORT'),
'transport' => [
'class' => 'Swift_SmtpTransport',
'host' => getenv('SMTP_HOST'),
'username' => getenv('SMTP_LOGIN'),
'password' => getenv('SMTP_PASSWORD'),
'port' => getenv('SMTP_PORT'),
'encryption' => 'ssl',
],
],
Кроме того, нам необходимо поправить входной скрипт web/index.php так, чтобы он забирал из переменных окружения режим работы приложения, заменяем первые строчки этими:
defined('YII_DEBUG') or define('YII_DEBUG', getenv('YII_DEBUG'));
defined('YII_ENV') or define('YII_ENV', getenv('YII_ENV'));
Теперь все готово чтобы выполнить миграции и проверить работу приложения в браузере.
$ docker-compose exec php ./yii migrate
После выполнения миграций, у меня успешно и без всяких ошибок открылся мой проект на локальном хосте 127.0.0.1:8888.
Тестирование с selenium в docker
Ранее, до того, как мы добавили докер в наше приложение, мы использовали разные конфиги приложения и базы для тестирования. Сейчас это не обязательно, проще и надежнее тестировать приложение с реальными конфигами, по необходимости немного их корректируя если приложение находится в режиме тестирования.
Главный конфиг codeception.yml я привожу к такому виду, чтобы он брал за основу /config/web.php вместо /config/test.php, как это было раньше:
actor: Tester
paths:
tests: tests
log: tests/_output
data: tests/_data
helpers: tests/_support
settings:
bootstrap: _bootstrap.php
memory_limit: 1024M
colors: true
modules:
config:
Yii2:
configFile: 'config/web.php'
Конкретно в этом приложении используется только acceptance тестирования, так как все его компоненты у меня во внешних библиотеках и тестируются там юнит-тестами отдельно. В этом конкретном случае, я хочу перед публикацией изменений приложения запустить acceptance тестирование, прокликать там основной функционал, и если никаких ошибок нет, отправить изменения в продакшн.
Когда мы писали docker-compose.dev.yml файл, мы добавили в него образ selenium/standalone-chrome, так что нам надо указать его в конфиге /tests/acceptance.suite.yml, а также указать, что обращаться к приложению нужно по адресу http://nginx. Этот конфиг у меня выглядит так:
class_name: AcceptanceTester
modules:
enabled:
- WebDriver:
url: http://nginx
host: selenium
browser: chrome
- Yii2:
part: [orm, email, fixtures]
cleanup: false
После этого можно запускать наши тесты:
$ docker-compose exec php vendor/bin/codecept run
Хочется отметить, что при тестировании в докере, мы не видим на нашем компьютере запущенного браузера, в котором пробегают тесты. Поэтому всю информацию придется брать из скриншотов и html-дампов в папке /tests/_output.
Помимо выполнения миграций, для проверки работы приемочных тестов, я предпочитаю подкинуть в приложение реальный дамп базы данных, поэтому возьму из бекапа любой дамп за последнее время и закину его в корень приложения, чтобы загружать его при запуске тестов в gitlab ci. Можно добавить его в репозиторий, что не очень уж красиво, можно брать его откуда-то в процессе выполнения паплайна в гитлабе. Я не буду выпендриваться, и добавлю в корень приложения облегченный и очищенный от лишних данных дамп с продакшена, и добавлю его в git.
Запуск тестов в gitlab-runner
Для начала, нам необходимо поставить себе локально gitlab runner. Поставить можно по инструкции для мака отсюда, или найти на этом сайте мануал для своей операционной системы.
Для того, чтобы у нас заработали gitlab piplines, необходимо в корень проекта добавить файл .gitlab-ci.yml, в котором описать инструкции для gitlab piplines. Кроме того, для того чтобы перед запуском тестов загружать в чистую базу данных наш снапшот боевой базы данных, я написал примитивный контроллер. Нужно это потому, что в нашем контейнере нет консольного mysql клиента, и загрузить этот дамп в базу консольной командой не получится. Вот код моего консольного контроллера для этих целей:
<?php
namespace app\commands;
use Yii;
use yii\console\Controller;
use yii\db\Exception;
/**
* Class DbController
* @package app\commands
*/
class DbController extends Controller
{
/**
* Загружаем базу для теста
* @throws Exception
*/
public function actionLoad()
{
$sql = file_get_contents('/app/test.dump.sql');
Yii::$app->db->createCommand($sql)->execute();
}
}
Теперь можно составить непосредственно файл инструкций на gitlab runner .gitlab-ci.yml, у меня он выглядит так:
codeception:
image: floor13/compose
services:
- docker:dind
variables:
DOCKER_DRIVER: overlay
DOCKER_HOST: tcp://docker:2375
artifacts:
expire_in: 1 week
when: on_failure
paths:
- tests/_output/*
script:
- cp docker-compose.dev.yml docker-compose.yml
- docker-compose up -d
- docker-compose exec -T php composer install -vv
- docker-compose exec -T php ./yii db/load
- docker-compose exec -T php vendor/bin/codecept run
Что же за образ такой floor13/compose? Это обычный alpine образ в котором уже есть docker-compose утилита. Сервис docker:dind - это специальный сервис, который позволяет нам запустить как бы докер в докере, ведь сам gitlab runner запускается в докере, а нам необходимо поднять в нем через docker-compose свои контейнеры. Собственно говоря, это и происходит далее в инструкциях: копирование конфига docker-compose.dev.yml, поднятие всей группы контейнеров, установка пакетов composer, загрузка тестовой базы и запуск codeception.
Проверяем работу конфиг файла запуском локального гитлаб раннера. Заходим в папку проекта и выполняем команду:
$ gitlab-runner exec docker --docker-privileged codeception
где codeception - это название нашей единственной работы, описанной в сценарии для гитлаба. Видим, что все выполняется хорошо, после чего коммитим и пушим изменения в гитлаб. Там, гитлаб обнаружив в корне проекта файл .gitlab-ci.yml, приступает к выполнению инструкций, описанных в нем.
Для обновления кода на проде я добавил в корень приложения небольшой баш-скрипт updade.sh, который забирает изменения с гитлаба, обновляет зависимости и накатывает миграции:
#!/bin/bash
git checkout -f master
git pull
docker-compose exec -T php composer install -vv
docker-compose exec -T php ./yii migrate --interactive=0
После этого, добавил соответствующую секцию в .gitlab-ci.yml, которая выполняет следующую инструкцию: подключается к продакшн серверу по ссш через ключ, который мы сохранили в переменной окружения в настройках CI репозитория в гитлабе, а после этого выполняет скрипт ./update.sh. Теперь .gitlab-ci.yml с двумя “работами” выглядит так:
codeception:
image: floor13/compose
services:
- docker:dind
variables:
DOCKER_DRIVER: overlay
DOCKER_HOST: tcp://docker:2375
artifacts:
expire_in: 1 week
when: on_failure
paths:
- tests/_output/*
script:
- cp docker-compose.dev.yml docker-compose.yml
- docker-compose up -d
- docker-compose exec -T php composer install -vv
- docker-compose exec -T php ./yii db/load
- docker-compose exec -T php vendor/bin/codecept run
prod_deploy:
stage: deploy
image: kroniak/ssh-client
only:
- master
before_script:
- mkdir -p ~/.ssh
- eval $(ssh-agent -s)
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
script:
- ssh-add <(echo "$PROD_PRIVATE_KEY" | base64 -d)
- ssh -p22 floor12@lg.ttit.pro "cd /home/floor12/youstore/ && ./update"
Таким образом, после того, как в браузере “прокликались” все тесты, раннер гитлаба подключится к продакшн серверу, и выполнит скрипт обновления, который накатит обновления из репозитория, обновит пакеты и накатит миграции.