Skip to content

Деплоймент PHP

За последние годы Continuous Integration и Continuous Delivery надёжно укоренились в процессах разработки приложений и стали маст‑хэв стандартом. Сейчас новаторы вовсю пробуют оседлать доставку приложений, завёрнутых в Docker, но об этом в следующий раз. А пока опишу процесс доставки кода до сервера с примерами, блэкджеком и сниппетами.

Почему PHP?

Потому что многие другие языки имеют уже написанные фреймворки для автоматизации этого процесса. Такой есть и для PHP, но о Deployer мы поговорим в следующий раз. Есть ещё возможность использовать Capistrano — гем, написанный на Ruby для Ruby on Rails. Собственно, по его типу и подобию написан и Deployer. Другие, более низкоуровневые фреймворки рассматривать не будем.

А почему Drupal 7?

Для 8‑ки есть более вкусные варианты, хотя этот способ тоже подойдёт, если у вас весь сайт целиком хранится в репозитории. Без всяких этих ваших Composer'ов.

А почему вообще Drupal?

Просто для примера.

В этот раз попробуем простой вариант, состоящий из двух частей:

  • bash‑скрипт, который будет запускаться на целевом сервере и выполнять сам деплоймент;
  • какой‑нибудь Continuous Integration (CI) — будет реагировать на новые коммиты и запускать процесс деплоймента.

Входящие данные

Чтобы не набрасываться на скрипт и не пытаться прикрутить его к своему приложению, разберёмся, что уже должно быть.

Что деплоить:

  • Для деплоймента возьмём сайт на Drupal 7, но подойдёт любое некомпилируемое приложение.

Откуда деплоить:

  • Репозиторий хранится в GitLab.
  • Для запуска CI будем использовать GitLab CI.

Куда деплоить:

  • К серверу есть SSH‑доступ.
  • Есть возможность изменять папку, где хостится сайт. А точнее — сделать её симлинком.

Структура на сервере

Обычно здесь предъявляются следующие пожелания:

  • Нужно версионирование, чтобы:
    1. Деплоймент должен проходить по принципу zero downtime.
    2. При необходимости можно было бы быстро откатиться на предыдущую версию.
    3. Сайт начинал работать на новой версии только после успешно завершённого деплоймента.
  • В файлах сайта могут лежать Секретные файлы.
  • Нужна директория с пользовательскими файлами, которая будет единственной для всех версий деплоймента.

Исходя из этого, и предположив, что сайт находится в /var/www/mysite, нарисуем такую структуру:

text
/var/www/mysite
  # Ссылка на последний успешный деплоймент/релиз
  ~current
  /repo                                                       <-- Клон Git репозитория
  /releases                                                   <-- Директории релизов
    /2019.03.25_20:13:12.84a10b1
    /2019.04.03_23:56:48.5a0ba30
    /2019.04.17_01:10:36.73215a0
  /raw                                                        <-- Здесь хранятся секретные данные, которые есть только на сервере.
                                                                  Содержимое будет копироваться в каждую папку релиза с заменой.
    /sites                                                    <-- Структура Drupal'а
      /default
        ~files -> /var/www/mysite/shared/storage              <-- Ссылка на папку с пользовательскими файлами.
        ~settings.php -> /var/www/mysite/shared/settings.php  <-- Ссылка на единый конфигурационный файл.
  /shared                                                     <-- Общие файлы, необходимые для всех релизов
    /storage                                                  <-- Пользовательские файлы
    settings.php

Визуально это выглядит так:

Деплоймент‑скрипт

Для совершения деплоймента можно использовать следующий скрипт:

bash
#!/usr/bin/env bash

set -e

# Following variables must be defined in your secret
# variables, like Environment variables of the GitLab.
# MAX_RELEASES=3
# DEPLOY_DIR="/var/www/mysite"
# DEPLOY_BRANCH="master"
# GIT_SOURCE="https://gitlab.com/<yourname>/<yoursite>.git"

CI_COLOR="\033[0;32m"
NO_COLOR="\033[0m"

GIT_DIR="$DEPLOY_DIR/repo"
RELEASES_DIR="$DEPLOY_DIR/releases"
RAW_DIR="$DEPLOY_DIR/raw"

print_title() {
  echo ""
  print_row "$@"
}

print_row() {
  echo -e "${CI_COLOR}$@${NO_COLOR}"
}

# Start deployment process.

print_title "Ensure required directories are exist."
mkdir -p "$DEPLOY_DIR"
mkdir -p "$GIT_DIR"
mkdir -p "$RELEASES_DIR"
mkdir -p "$RAW_DIR"
echo "Done"

print_title "Ensure repository exist."
cd "$GIT_DIR"
if [[ ! $(git status 2>/dev/null) ]]; then
  echo "Clone repository"
  cd "../" && rm -rf "$GIT_DIR" && git clone "$GIT_SOURCE" "$GIT_DIR"
  cd "$GIT_DIR"
  echo "Done"
fi

print_title "Update repository"
git fetch --all
git reset --hard "origin/$DEPLOY_BRANCH"
echo "Done"

COMMIT=$(git log -n1 --abbrev-commit | grep ^commit | awk '{print $2}')
TIMESTAMP=$(date +%Y.%m.%d_%H:%M:%S)
RELEASE="$TIMESTAMP.$COMMIT"
RELEASE_DIR="$RELEASES_DIR/$RELEASE"

print_title "Create directory for the new release \"$RELEASE\""
mkdir -p "$RELEASE_DIR" && echo "Done"

print_title "Deploy the release \"$RELEASE\""
rsync -a --stats --inplace "$GIT_DIR/" "$RELEASE_DIR" --exclude=".git" && echo "Done"

print_title "Copy raw files"
rsync -al --force --stats "$RAW_DIR/" "$RELEASE_DIR" && echo "Done"

print_title "Create symlink to the release \"$RELEASE\""
ln -sfn "$RELEASE_DIR" "$DEPLOY_DIR/current" && echo "Done"

cd "$RELEASES_DIR"
if [ "$(ls -l | grep -c ^d)" -gt "$MAX_RELEASES" ]; then
  print_title "Delete deprecated versions"
  while [ "$(ls -l | grep -c ^d)" -gt "$MAX_RELEASES" ]; do
    DEPRECATED_RELEASE=$(ls -r | tail -n 1)
    chmod 755 -R "./$DEPRECATED_RELEASE"
    rm -rf "./$DEPRECATED_RELEASE"
    echo "Version \"$DEPRECATED_RELEASE\" has been deleted"
  done
  echo "Done"
else
  print_title "Deprecated versions not found"
fi

print_title "The deployment completed successfully"

Для скрипта необходимо иметь объявленные переменные окружения:

  • MAX_RELEASES — количество допустимых релизов для хранения (пример: 3)
  • DEPLOY_DIR — директория для развёртывания всего этого добра (пример: /var/www/mysite)
  • DEPLOY_BRANCH — название ветки, которую нужно вытягивать из репозитория для деплоймента (пример: master)
  • GIT_SOURCE — адрес репозитория для клонирования (сервер должен иметь доступ до репозитория)

Кладём этот скрипт в папку проекта scripts/deploy.sh. Создаём CI‑конфигурацию в файле .gitlab-ci.yml в корне проекта:

yaml
image: php:7.1.1

stages:
  - deploy

Deploy Production:
  stage: deploy
  before_script:
    - apt-get update -yqq
    - apt-get install libmcrypt-dev openssh-client -y
    - eval "$(ssh-agent -s)"
    - echo "$DEPLOY_RSA" | tr -d '\r' | ssh-add - 2>/dev/null
    - mkdir -m 700 -p ~/.ssh
  script:
    - MAX_RELEASES="$MAX_RELEASES" \
      DEPLOY_DIR="$DEPLOY_DIR" \
      DEPLOY_BRANCH="$DEPLOY_BRANCH" \
      GIT_SOURCE="$GIT_SOURCE" \
      ssh -o StrictHostKeyChecking=no -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_SERVER" \
      'bash --login -s' < scripts/deploy.sh
  only:
    - master

Эта конфигурация говорит о том, что нужно запускать деплоймент при каждом коммите в ветку master.

Ещё один штрих — переменные окружения должны быть настроены в GitLab. Рекомендую использовать защищённые переменные, чтобы никто, кроме админов, не смог увидеть их.

Кроме переменных, необходимых для деплоймент‑скрипта, также нужно настроить переменные SSH‑доступа до сервера:

  • DEPLOY_USER — имя пользователя, из‑под которого будет деплоиться аппликация.
  • DEPLOY_RSA — приватный ключ пользователя, из‑под которого будет производиться деплоймент;
  • DEPLOY_PORT — SSH‑порт сервера;
  • DEPLOY_SERVER — адрес сервера, куда будет деплоиться приложение;

На этом всё. Результат получится примерно следующий: