В статье я поделюсь своим опытом автоматизации всего процесса разработки приложения 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

Настройка сервера с 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 для 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 для разработки


Нужно выполнить все пункты установки 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_...).


Настройка внешнего Nginx

Создание конфигурационного файла


Пример конфигурация для домена 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.


Вместе с изменением в коде проекта, разработчик может добавлять новые значения в параметры приложения. Значения этих параметров могут отличаться в зависимости от окружения.


Настройка параметров Symfony

Локальное окружение разработчика


Дефолтная конфигурация хранится в файле .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 stage в .gitlab-ci.yml
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.


Этап test в .gitlab-ci.yml
.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.


Этап build в .gitlab-ci.yml
.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.


Пример сформированного файла docker-compose.yml
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.


Этап deploy в .gitlab-ci.yml
.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, или оставим всё как есть. Пока не ясно, время покажет.

Комментарии (38)