В статье я поделюсь своим опытом автоматизации всего процесса разработки приложения Symfony с нуля от настройки инфраструктуры до деплоя в production. От development- и до production-окружения для запуска приложения будет использоваться docker-compose, а все процедуры непрерывной интеграции/внедрения будут запускаться через GitLab CI/CD Pipelines в docker-контейнерах.
Подразумевается, что вы знакомы с docker и docker-compose. Если нет или вы не знаете как его установить, я подготовил инструкцию по подготовке локального окружения разработчика. Фактически, для работы над приложением потребуется только Docker, VirtualBox и, опционально, Yarn.
Запуск приложения локально
Я подготовил скелет приложения и выложил его на GitHub. Всё написанное ниже относится к приложениям, созданным на основе этого шаблона и к инфраструктуре, необходимой для запуска такого приложения.
Чтобы запустить приложение локально, нужно выполнить следущие команды:
git clone git@github.com:covex-nn/docker-workflow-symfony.git
cd docker-workflow-symfony
docker-compose up -d
docker-compose exec php phing
Сайт будет доступен по адресу http://docker.local/, добавлять app_dev.php/
к адресу не нужно. Будет запущено 4 контейнера: nginx
, php
, mysql
и phpmyadmin
(последний запускается только в development-окружении).
docker.local
нужно прописать в файл hosts
. Для Linux ip-адрес сайта будет 127.0.0.1
, а под Windows его можно узнать в результате работы команды docker-machine env
(всё таки см. инструкцию).
composer
в контейнере php
настроен таким образом, что папка vendor
находится внутри контейнера, а не на хосте, и не оказывает влияние на быстродействие в локальном окружении разработчика.
Подготовка и настройка инфраструктуры
В боевых условиях для работы системы потребуется три сервера: GitLab
— сервер для управления репозиториями Git и Container Registry, Docker для production
— сервер для production-сайтов и Docker для разработки
— сервер для pre-production и тестовых сайтов разработчиков.
Настройка сервера с GitLab и Container Registry
С инструкциями по установке GitLab и Container Registry можно ознакомиться на сайте gitlab.com.
По умолчанию для GitLab Container Registry требуется настройка SSL сертификатов. Мы будем использовать один и тот же сертификат и для Container Registry, и для Web-интерфейса GitLab. Создать SSL-сертификат можно с помощью сервиса LetsEncrypt.
Подключить SSL-сертификат можно в файле /etc/gitlab/gitlab.rb
. Также нужно настроить возможность автоматического обновления сертификата:
nginx['ssl_certificate'] = "/etc/letsencrypt/live/gitlab.site.ru/fullchain.pem"
nginx['ssl_certificate_key'] = "/etc/letsencrypt/live/gitlab.site.ru/privkey.pem"
registry_nginx['ssl_certificate'] = "/etc/letsencrypt/live/gitlab.site.ru/fullchain.pem"
registry_nginx['ssl_certificate_key'] = "/etc/letsencrypt/live/gitlab.site.ru/privkey.pem"
nginx['custom_gitlab_server_config'] = "location ^~ /.well-known { \n allow all;\n alias /var/lib/letsencrypt/.well-known/;\n default_type \"text/plain\";\n try_files $uri =404;\n }\n"
После изменения в файле gitlab.rb
нужно перегрузить GitLab через gitlab-ctl restart
и настроить crontab
для обновления сертификатов:
41 0 * * * /root/certbot-auto renew --no-self-upgrade --webroot -w /var/lib/letsencrypt --renew-hook "service nginx reload"
Настройка сервера с Docker для production
С инструкцией по установке Docker можно ознакомиться на сайте docs.docker.com.
Дополнительно нужно создать локальную сеть для назначения контейнерам внутренних IP адресов:
docker network create graynetwork --gateway 192.168.10.1 --subnet 192.168.10.0/24
Кроме Docker на сервер нужно установить nginx
и certbot-auto
от LetsEncrypt.
Nginx будет проксировать запросы к веб-серверам в контейнерах Docker. С инструкцией по установке Nginx можно ознакомиться на сайте nginx.org.
Обновление будущих SSL-сертификатов должно быть настроено сразу же так, как с на сервере с GitLab:
41 0 * * * /root/certbot-auto renew --no-self-upgrade --webroot -w /var/lib/letsencrypt --renew-hook "service nginx reload"
Настройка сервера с Docker для разработки
Нужно выполнить все пункты установки Docker для production
и дополнительно на сервер нужно установить GitLab CI Runner
.
С инструкцией по установке GitLab CI Runner
можно ознакомиться на сайте docs.gitlab.com.
Запуск GitLab Runner:
gitlab-ci-multi-runner verify --delete
printf "concurrent = 10\ncheck_interval = 0\n\n" > /etc/gitlab-runner/config.toml
gitlab-ci-multi-runner register -n --url https://gitlab-server.ru/ --registration-token <token> --tag-list "executor-docker,docker-in-docker" --executor docker --description "docker-dev" --docker-image "docker:latest" --docker-volumes "/composer/home/cache" --docker-volumes "/root/.composer/cache" --docker-volumes "/var/run/docker.sock:/var/run/docker.sock"
Токен <token>
нужно скопировать из Web-интерфейсе GitLab в разделе Admin Area --> Runners
.
Над проектом будут работать несколько разработчиков, им нужно выдать доступ так, чтобы они ничего не сломали и не мешали друг другу.
Создание Master-пользователя
На сервере
Docker для production
нужно создать пользователяmaster
и добавить в группуdocker
:
adduser master usermod -aG docker master
Далее нужно зайти под новым пользователем и создать
id_rsa
ключ без passphrase:
ssh-keygen -t rsa -b 4096 -C "master@docker-server-prod.ru" cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
Этот ключ будет использоваться для SSH-доступа на сервер и для доступа в git-репозитории разработчиков.
- В GitLab создать пользователя
master
и добавить ему SSH-ключ. Этот пользователь будет чисто техническим. В дальнейшем под ним не нужно будет заходить и выполнять какие-либо операции.
Создание пользователя-разработчика
На сервере
Docker для разработки
нужно создать пользователяdev1
(имя может быть любым):
adduser dev1 usermod -aG docker dev1
Далее нужно зайти под новым пользователем и создать id_rsa ключ без passphrase:
ssh-keygen -t rsa -b 4096 -C "dev1@docker-server-dev.ru" cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys chmod 400 ~/.ssh/id_rsa ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys
Этот ключ будет использоваться для SSH-доступа на сервер, он не должен быть известен кому-либо из разработчиков.
В GitLab создать пользователя
dev1
, запретив ему создавать свои репозитории и группы. SSH-ключ настраивать не нужно — разработчик сам себе его настроит.
- В GitLab создать группу
dev1-projects
и добавить в группу пользователяmaster
с рольюMaster
. В этой группе будут находиться все репозитории данного разработчика.
У проекта будет один основной репозиторий и по одному репозиторию на каждого разработчика. Основной репозиторий будет источником для production- и staging-сайтов, репозиторий разработчика – для тестового сайта именно этого разработчика. Процессы деплоя для каждого из сайтов будут совпадать. Отличия будут только в конфигурации приложения и настройках доступа к серверу c Docker. Конфигурация и настройки будут храниться в GitLab
, в разделе Settings -- CI/CD Pipelines
: в основном репозитории – для production- и staging-сайтов, а в репозитории разработчика – для тестового сайта этого разработчика.
Основной репозиторий проекта можно поместить в произвольную группу.
В разделе Settings --> Pipelines
нужно выбрать git clone
в качестве Git strategy for pipelines
и добавить переменные:
Переменная | Значение |
---|---|
COMPOSER_GITHUB_TOKEN | Создать токен на странице https://github.com/settings/tokens |
SSH_PRIVATE_KEY | заполнить её содержимым файла id_rsa пользователя master |
NETWORK_NAME_MASTER | graynetwork |
SERVER_NAME_MASTER | site-staging.ru |
NETWORK_IP_MASTER | выбрать свободный IP в подсети graynetwork |
NETWORK_NAME_PRODUCTION | graynetwork |
SERVER_NAME_PRODUCTION | site-production.ru |
NETWORK_IP_PRODUCTION | выбрать свободный IP в подсети graynetwork |
DEPLOY_USER_MASTER | master |
DEPLOY_HOST_MASTER | docker-server-prod.ru |
DEPLOY_DIRECTORY_MASTER | /home/master/site-staging.ru |
DEPLOY_USER_PRODUCTION | master |
DEPLOY_HOST_PRODUCTION | docker-server-prod.ru |
DEPLOY_DIRECTORY_PRODUCTION | /home/master/site-production.ru |
PROJECT_FORKS | <оставить пустым> |
Для деплоя скелета приложения на staging нужно залить ветку master
в репозиторий через git push origin master
.
Репозиторий разработчика должен находиться в группе проектов разработчика. Для пользователя dev1
— это dev1-projects
. Репозиторий разработчика создаётся путём создания Fork администратором из основного репозитория. Это важно.
- Вместе с созданием fork появится возможность создавать Merge Request из репозитория разработчика в основной
- А создание fork именно администратором необходимо для обеспечения стабильности работы системы и сохранения в секрете id_rsa ключа для доступа на сервер.
В разделе Settings --> Pipelines
нужно выбрать git clone
в качестве Git strategy for pipelines
, скрыть Public pipelines
и добавить переменные:
Переменная | Значение |
---|---|
COMPOSER_GITHUB_TOKEN | Создать токен на странице https://github.com/settings/tokens |
SSH_PRIVATE_KEY | заполнить её содержимым файла id_rsa пользователя dev1 |
NETWORK_NAME_MASTER | graynetwork |
SERVER_NAME_MASTER | site-dev1.ru |
NETWORK_IP_MASTER | выбрать свободный IP в подсети graynetwork |
DEPLOY_USER_MASTER | dev |
DEPLOY_HOST_MASTER | docker-server-dev.ru |
DEPLOY_DIRECTORY_MASTER | /home/dev1/site-dev1.ru |
PROJECT_FORKS | <оставить пустым> |
Перед деплоем на тестовый сайт, нужно создать ветку stable
, указывающую в тот же коммит, что и ветка master
. Ветка stable
будет соответствовать состоянию staging-сайта, в этой ветке будет находиться только проверенный и принятый код.
В процессе работы разработчик должен, с одной стороны, иметь возможность объединять коммиты и переписывать историю через git push -f origin master
. А с другой стороны, он не должен иметь возможность смещать ветку stable
и создавать тэги, чтобы не нарушить работу всей остальной системы.
Для этого в разделе Settings --> Repository
нужно снять защиту с ветки master
и защить ветку stable
и все тэги.
Для деплоя приложения на тестовый сайт разработчика нужно запустить Pipeline для ветки master
. После этого нужно выдать роль Developer
пользователю dev1
в разделе Settings --> Members
.
В конце нужно донастроить основной репозиторий. Нужно добавить строку с адресом репозитория разработчика в переменную PROJECT_FORKS
для синхронизации ветки stable
в новом репозитории. И выдать роль Reporter
пользователю dev1
в основном репозитории.
Последний шаг до начала работы – настройка Nginx на серверах с Docker. Этот Nginx будет настраиваться вручную, и все HTTP/HTTPS-запросы к приложениям Symfony будут им проксироваться в выбранный IP-адрес во внутренней, ранее созданной, подсети Docker (см. переменные NETWORK_NAME_...
и NETWORK_IP_...
).
Создание конфигурационного файла
Пример конфигурация для домена site-dev1.ru
. Здесь 192.168.10.10
— содержимое переменной NETWORK_IP_MASTER
из настроек репозитория разработчика dev1
.
server {
listen 80;
# listen 443 ssl;
server_name site-dev1.ru;
# ssl_certificate /etc/letsencrypt/live/site-dev1.ru/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/site-dev1.ru/privkey.pem;
# if ($ssl_protocol = "") {
# rewrite ^/(.*) https://$server_name/$1 permanent;
# }
location / {
proxy_pass http://192.168.10.10;
include proxy_params;
}
location ~ /.well-known {
allow all;
alias /var/lib/letsencrypt/.well-known;
}
}
Создание SSL-сертификата
/root/certbot-auto certonly --no-self-upgrade --webroot -d site-dev1.ru -w /var/lib/letsencrypt
Для переключения сайта с HTTP на HTTPS нужно раскоментировать строки в конфигурации HTTP-домена и перегрузить Nginx.
nginx -t
service nginx reload
Процесс разработки
На данном этапе у разработчика есть доступ в свой собственный репозиторий. В своём репозитории он имеет роль Developer
и может делать практически всё что угодно. В репозитории разработчика ветка master
соответствует состоянию его тестового сайта. Protected-ветка stable
— состоянию сайта staging
.
Каждое новое задание должно начинаться с создания ветки задачи, указывающей на тот же коммит, что и ветка stable
.
git fetch --all --prune
git checkout origin/stable
git checkout -b feature-qwerty
git push origin feature-qwerty
Затем, на каком-то этапе, когда нужно выложить свои изменения в на тестовый сайт, можно залить изменения в репозиторий в ветку master
— и изменения будут выложены в течении 2-5 минут.
Слияние изменений из репозитория разработчика в основной должно происходить из ветки задачи, в примере — это feature-qwerty
, в ветку master
основного репозитория через создания соответствующего Merge Request в Web-интерфейсе GitLab.
Перед принятием Merge Request администратор должен убедиться, что коммиты в ветке разработчика идут строго после текущего положения ветки master
основного репозитория. Автоматически в GitLab CE это сделать не получится, фича доступна только в GitLab EE.
Для выкатки изменений на рабочий сайт, нужно создать тэг release-...
в Web-интерфейсе GitLab.
Вместе с изменением в коде проекта, разработчик может добавлять новые значения в параметры приложения. Значения этих параметров могут отличаться в зависимости от окружения.
Локальное окружение разработчика
Дефолтная конфигурация хранится в файле .env
в корне проекта. Этот файл — один на всех разработчиков и является частью репозитория:
ENV_hwi_facebook_client_id=1234
ENV_hwi_facebook_client_secret=4567
Файл загружается при запуске docker-compose up -d
, значения попадают в контейнер через блок environment
в описании сервиса php
:
services:
php:
environment:
ENV_hwi_facebook_client_id: "${ENV_hwi_facebook_client_id}"
ENV_hwi_facebook_client_secret: "${ENV_hwi_facebook_client_secret}"
Внутри Symfony эти значения попадают через файл app/config/parameters.yml
(он также является частью приложения):
parameters:
hwi_facebook_client_id: "%env(ENV_hwi_facebook_client_id)%"
env(ENV_hwi_facebook_client_id): ~
hwi_facebook_client_secret: "%env(ENV_hwi_facebook_client_secret)"
env(ENV_hwi_facebook_client_secret): ~
Для внедрения новых папаметров, нужно перезагрузить docker-compose
:
docker-compose stop
docker-compose up -d
Тестовый сайт разработчика
Перед выкаткой изменений на тестовый сайт разработчика, администратор должен добавить значения переменных для этого сайта в разделе Settings --> Pipelines
. К именам переменных должен быть добавлен суффикс _MASTER
ENV_hwi_facebook_client_id_MASTER
ENV_hwi_facebook_client_secret_MASTER
Если переменные не будут созданы, значения для них будут браться из файла .env
.
Staging
До принятия Merge Request в основном репозиторий нужно добавить переменные с суффиксом _MASTER
, как это было сделано для тестового сайта разработчика.
После принятия Merge Request и внедрения изменений на staging
нужно добавить переменные во все остальные репозитории разработчиков.
Production
В основной репозиторий нужно добавить переменные с суффиксом _PRODUCTION
, как это было сделано для staging.
Также для разработчика в development-окружении доступно расширение xdebug
, а управление CSS и Javascript файлами происходит при помощи Webpack Encore.
CI/CD изнутри
Процесс непрерывной интеграции/внедрения описан в файле .gitlab-ci.yml в корне репозитория, он состоит из 4 стадий: загрузка зависимостей, phpunit-тестирование, сборка, развёртывание.
Загрузка зависимостей
На данном этапе производится попытка установить все зависимости приложения через composer
.
deps:php-composer:
stage: deps
image: covex/php7.1-fpm:1.0
script:
- echo '{"github-oauth":{"github.com":"'"$COMPOSER_GITHUB_TOKEN"'"}}' > ./auth.json
- composer install --prefer-dist --no-scripts --no-autoloader --no-interaction
tags:
- executor-docker
Результатом работы данного этапа будет наполнение папки /composer/home/cache
. Эта папка сохраняется в volume
у gitlab-ci-multi-runner
и кэш composer будет доступен при выполнении всех последующих задач (как в текущей pipeline, так и в последующих).
PHPUnit-тестирование
Перед запуском собственно phpunit
, создаются переменные окружения для работы приложения Symfony. Если какие-то значения переменных в testing-окружении должны отличаться значений во всех остальных окружениях — нужно создать такие переменные в настройках репозитория GitLab с суффиксом _TEST
(например, ENV_hwi_facebook_client_id_TEST
). Тогда её значение перекроет дефолтное из файла .env
.
.template-suffix-vars: &suffix-vars
before_script:
- cat .env | grep ENV_ > .build-env
- sed -i 's/^/export /' .build-env
- for name in `env | awk -F= '{if($1 ~ /'"$ENV_SUFFIX"'$/) print $1}'`; do
echo 'export '`echo $name|awk -F''"$ENV_SUFFIX"'$' '{print $1}'`'='`printenv $name`'' >> .build-env;
done
test:phpunit:
stage: test
image: covex/php7.1-fpm:1.0
<<: *suffix-vars
variables:
ENV_SUFFIX: "_TEST"
script:
- eval $(cat .build-env)
- echo '{"github-oauth":{"github.com":"'"$COMPOSER_GITHUB_TOKEN"'"}}' > ./auth.json
- composer require phpunit/phpunit:* --dev
- phpunit
dependencies: []
tags:
- executor-docker
Сборка
Здесь сборка для php-проекта — это создание docker-образов для контейнеров nginx и php, и выкладывание подготовленных образов в GitLab Container Registry.
.template-docker-nginx-image: &docker-nginx-image
stage: build
image: docker:latest
<<: *suffix-vars
script:
- eval $(cat .build-env)
- docker build --tag $CI_NGINX_IMAGE_WITH_TAG --build-arg server_name=$SERVER_NAME --build-arg server_upstream=prod --build-arg app_php=app ./docker/nginx
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker push $CI_NGINX_IMAGE_WITH_TAG
- docker logout $CI_REGISTRY
tags:
- executor-docker
- docker-in-docker
.template-docker-app-image: &docker-app-image
stage: build
image: docker:latest
<<: *suffix-vars
script:
- eval $(cat .build-env)
- echo '{"github-oauth":{"github.com":"'"$COMPOSER_GITHUB_TOKEN"'"}}' > ./auth.json
- docker build --tag $CI_APP_IMAGE_WITH_TAG .
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker push $CI_APP_IMAGE_WITH_TAG
- docker logout $CI_REGISTRY
dependencies:
- deps:php-composer
tags:
- executor-docker
- docker-in-docker
.template-docker-compose: &docker-compose
stage: build
image: covex/docker-compose:1.0
<<: *suffix-vars
script:
- eval $(cat .build-env)
- mkdir build
- docker-compose -f docker-compose-deploy.yml config > build/docker-compose.yml
- sed -i 's/\/builds\/'"$CI_PROJECT_NAMESPACE"'\/'"$CI_PROJECT_NAME"'/\./g' build/docker-compose.yml
artifacts:
untracked: true
name: "$CI_COMMIT_REF_NAME"
paths:
- build/
tags:
- executor-docker
dependencies: []
build:docker-nginx-image-master:
<<: *docker-nginx-image
variables:
ENV_SUFFIX: "_MASTER"
only:
- master
except:
- tags
build:docker-nginx-image-production:
<<: *docker-nginx-image
variables:
ENV_SUFFIX: "_PRODUCTION"
only:
- /^release-.*$/
except:
- branches
build:docker-app-image-master:
<<: *docker-app-image
variables:
ENV_SUFFIX: "_MASTER"
only:
- master
except:
- tags
build:docker-app-image-production:
<<: *docker-app-image
variables:
ENV_SUFFIX: "_PRODUCTION"
only:
- /^release-.*$/
except:
- branches
build:docker-compose-master:
<<: *docker-compose
variables:
ENV_SUFFIX: "_MASTER"
only:
- master
except:
- tags
build:docker-compose-production:
<<: *docker-compose
variables:
ENV_SUFFIX: "_PRODUCTION"
only:
- /^release-.*$/
except:
- branches
Здесь, задача build:docker-app-image-master
создаёт образы PHP-приложения для staging-сайта (и для тестового сайта разработчика); а задача build:docker-app-image-production
— для production-сайта. Для каждой задачи значения переменных из настроек pipeline с суффиксом _MASTER
или _PRODUCTION
перекрывают дефолтные значения из файла .env
. Аналогичным образом описаны задачи по сборке образов nginx
(см. задачи build:docker-nginx-image-master
и build:docker-nginx-image-production
).
Также на этом этапе создаётся файл docker-compose.yml
, который на следующем этапе будет скопирован на удалённый сервер (см. задачи build:docker-compose-master
и build:docker-compose-production
). Сформированный файл docker-compose.yml
содержит все переменные окружения, необходимые для запуска приложения. В секции services
все контейнеры будут создаваться только на основе готовых образов docker.
networks:
nw_external:
external:
name: graynetwork
nw_internal: {}
services:
mysql:
environment:
MYSQL_DATABASE: project
MYSQL_PASSWORD: project
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: project
expose:
- '3306'
image: covex/mysql:5.7
networks:
nw_internal: null
restart: always
volumes:
- database:/var/lib/mysql:rw
nginx:
depends_on:
mysql:
condition: service_healthy
image: gitlab.site.ru:5005/dev1-projects/symfony-workflow2/nginx:master
networks:
nw_external:
ipv4_address: 192.168.10.13
nw_internal: null
ports:
- 80/tcp
restart: always
volumes:
- assets:/srv/a:ro
- assets:/srv/b:ro
- assets:/srv/storage:ro
php:
environment:
ENV_database_host: mysql
ENV_database_mysql_version: '5.7'
ENV_database_name: project
ENV_database_password: project
ENV_database_port: '3306'
ENV_database_user: project
ENV_mailer_from: andrey@mindubaev.ru
ENV_mailer_host: 127.0.0.1
ENV_mailer_password: 'null'
ENV_mailer_transport: smtp
ENV_mailer_user: 'null'
ENV_secret: ThisTokenIsNotSoSecretChangeIt
image: gitlab.site.ru:5005/dev1-projects/symfony-workflow2:master
networks:
nw_internal: null
restart: always
volumes:
- assets:/srv/a:rw
- assets:/srv/b:rw
- assets:/srv/storage:rw
spare:
environment:
ENV_database_host: mysql
ENV_database_mysql_version: '5.7'
ENV_database_name: project
ENV_database_password: project
ENV_database_port: '3306'
ENV_database_user: project
ENV_mailer_from: andrey@mindubaev.ru
ENV_mailer_host: 127.0.0.1
ENV_mailer_password: 'null'
ENV_mailer_transport: smtp
ENV_mailer_user: 'null'
ENV_secret: ThisTokenIsNotSoSecretChangeIt
image: gitlab.site.ru:5005/dev1-projects/symfony-workflow2:master
networks:
nw_internal: null
restart: always
volumes:
- assets:/srv/a:rw
- assets:/srv/b:rw
- assets:/srv/storage:rw
version: '2.1'
volumes:
assets: {}
database: {}
Развёртывание
На данном этапе docker-образы приложения готовы и загружены в Container Registry. Осталось обновить приложения.
На удалённых серверах ceрвис phpmyadmin
отсутствует; дополнительно к сервису php
добавлен абсолютно такой же сервис spare
; а в конфигурации nginx
вместо одного сервера в upstream
прописано два. Использование двух одинаковых сервисов позволило добиться практически нулевого deployment downtime.
.template-secure-copy: &secure-copy
stage: deploy
image: covex/alpine-git:1.0
before_script:
- eval $(ssh-agent -s)
- ssh-add <(echo "$SSH_PRIVATE_KEY")
script:
- eval $(cat .build-env)
- ssh -p 22 $DEPLOY_USER@$DEPLOY_HOST 'set -e ;
rm -rf '"$DEPLOY_DIRECTORY"'_tmp ;
mkdir -p '"$DEPLOY_DIRECTORY"'_tmp'
- scp -P 22 -r build/* ''"$DEPLOY_USER"'@'"$DEPLOY_HOST"':'"$DEPLOY_DIRECTORY"'_tmp'
- ssh -p 22 $DEPLOY_USER@$DEPLOY_HOST 'set -e ;
if [ -d '"$DEPLOY_DIRECTORY"' ]; then rm -rf '"$DEPLOY_DIRECTORY"'; fi ;
mv '"$DEPLOY_DIRECTORY"'_tmp '"$DEPLOY_DIRECTORY"' ;
cd '"$DEPLOY_DIRECTORY"' ;
docker login -u gitlab-ci-token -p '"$CI_JOB_TOKEN"' '"$CI_REGISTRY"' ;
docker-compose pull ;
docker-compose up -d --no-recreate ;
docker-compose up -d --force-recreate --no-deps spare ;
docker-compose exec -T spare sh -c "cd /srv && rm -rf b/* && cp -a web/. b/ && rm -rf a/* && cp -a web/. a/" ;
docker-compose exec -T spare phing storage-prepare database-deploy ;
docker-compose up -d --force-recreate --no-deps php'
- ssh -p 22 $DEPLOY_USER@$DEPLOY_HOST 'set -e ;
cd '"$DEPLOY_DIRECTORY"' ;
echo "[$(date -R)] web-server is down" ;
docker-compose stop nginx ;
docker-compose up -d nginx ;
echo "[$(date -R)] web-server is up"'
tags:
- executor-docker
deploy:secure-copy-master:
<<: *secure-copy
only:
- master
except:
- tags
environment:
name: staging
dependencies:
- build:docker-compose-master
deploy:secure-copy-production:
<<: *secure-copy
only:
- /^release-.*$/
except:
- branches
environment:
name: production
dependencies:
- build:docker-compose-production
Алгоритм развёртывания следующий:
- Копируем сформированный на этапе
build
файлdocker-compose.yml
- Загружаем новые образы из Container Registry
- Обновляем контейнер
spare
- Обновляем статичные файлы для
nginx
, производим миграцию БД - Обновляем контейнер
php
- Обновляем контейнеры
nginx
иmysql
(в боевых условиях — это не обязательно)
Во время обновления контейнеров spare
или php
, nginx
через несколько секунд недоступности одного из них переключается на следующий доступный в upstream
. Т.е. приложение работает правильно для 100% HTTP-запросов, но иногда с задержкой.
Во время выполнения миграции БД первая половина HTTP-запросов идёт в контейнер php
, который может работать со старой структурой БД, а вторая половина — в контейнер spare
, который может работать с только с новой структурой. Т.е. в обоих контейнерах возможны сбои в работе во время миграции БД. Но если допустить, что внесение изменений в структуру БД не такое уж и частое явление, то можно считать это вполне приемлемым.
Во время обновления контейнеров nginx
и mysql
, сайт недоступен вообще. Эти сервисы обновляются очень редко, обновление вообще можно производить вручную "ночью". Проверка возможности обновлений для этих контейнеров длится около 5 секунд, что примерно 80-90% от всего deployment downtime.
Заключение
GitLab Continuous Integration & Deployment
и docker-compose
— замечательные инструменты. Вместе с ними мы наконец-то смогли отказаться от использования vagrant
в процессе разработки. Сайт проекта, запущенный локально, стал работать гораздо быстрее, даже с большим количеством библиотек, подключенных через composer.json
. Development-окружение стало не просто схожим — теперь оно абсолютно такое же, как в production, исчезли ограничения использования технологий помимо Linux + Apache + PHP + MySQL. Параллельные изменения в коде разными разработчиками не конфликтуют друг с другом, а новая процедура деплоя позволяет выкладывать изменения гораздо чаще, чем мы могли бы себе позволить ранее.
Следующий шаг — использование docker swarm
, или kubernetes
, или оставим всё как есть. Пока не ясно, время покажет.