Речь пойдет про простой инструмент для развертывания (деплоя) сайтов и сервисов на Linux системы. Сделали сами, пользуемся несколько лет, при определенных обстоятельствах может пригодится и вам, пользуйтесь.

Состоит из пары bash скриптов (интегрируются в исходный код проекта), а для их работы нужны только стандартные утилиты Linux (наличие git на целевых серверах не требуется).

Обеспечивает:
- развертывание вашего проекта на любое количество серверов
- проверка корректности среды для развертывания
- детектирование локальных изменений файлов
- возможность полностью автоматически последовательно вернуться на три предыдущие версии
- очистку сервера от старых, неиспользуемых версий
- легкую интеграцию в проект
- совместимость с любой системой CI, которая поддерживает запуск bash скриптов

Подходит для:
- веб сайтов, работающих под управлением ngnix, apache, и вообще, любого веб-сервера, у которого понятие «сайт» - это каталог где расположен корень сайта (doc_root)
- сервисов, представляющих собой запускаемый модуль (бинарный или скриптовый)
- проекта, которые развертывается на целевую систему в виде конкретного каталога или файла (при этом он может иметь взаимосвязи с другими каталогами системы без ограничений) и у проекта есть «точка входа» которая полностью идентифицируется этим каталогом или файлом

На картинке это можно представить вот так:

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

Зачем это надо, если есть Docker и git и другие инструменты?

Главная фишка тут – почти полная независимость от внешней среды. Из внешних зависимостей тут только Linux, bash и их стандартные утилиты и команды. А они как известно поддерживают крайне стабильную совместимость между системами и поколениями. Это может сыграть ключевую роль там, необходимо поставлять проект на самые разные системы/сервера, когда у вас нет возможности обеспечить на них совместимую среду для более сложных пакетов (даже python скрипты не будут работать где угодно!), и там, где нет ресурсов (сил, желания) тянуть проект на постоянно развивающихся (и не всегда заботящихся об обратной совместимости) системах CI и DevOps.

Второе, но не менее важное – это отсутствие backdoor для доступа к вашему репозитарию извне, т.к. система не использует git на серверах, куда деплоит проект.

Ну и третье, если Вам просто невозможно использовать что-то другое (заказчик запретил, система не поддерживает, и другие, экзотические случаи).

Мы сами являемся сторонниками перехода на доставку с помощью Docker-а и сейчас переводим наши основные проекты на него, но вот старые, те что живут на одной поддержке и не планируются к развитию, оставляем на этом инструменте, который был разработан и внедрен пару лет назад, и успешно эксплуатируется все это время в связке со всеми перечисленными в статье инструментами и технологиями.

Быстрый старт

Для того, чтобы это полностью заработало, нужно сделать следующие шаги:

  1. Включить скрипты сборки установочного пакета и деплоя в репозитарий вашего проекта (0.5-10 минут)
  2. Настроить эти скрипты для работы с инфраструктурой проекта (для первого раза 30-60 минут, со второго минут 10-15)
  3. Подготовить на серверах для деплоя структуру каталогов для работы скрипта (для первого раза с полчаса, дальше не более нескольких минут).

    И с этого момента оно уже работает, но для полноценной интеграции в CI конечно еще нужно:
  4. Если проект имеет сложную инфраструктуру (внешние зависимости, создает временные файлы внутри себя, и т.д. и т.п.), потребуется чуть более тонкая настройка – делаем ее и отлаживаем скрипты до полной работоспособности
  5. Включить запуск скриптов сборки и деплоя в вашу CI в соответствии с вашим workflow (это может быть любая система, которая поддерживает запуск bash скриптов, например, Gitlab CI или Jenkins)
  6. И написать краткую инструкцию для разработчиков как этим пользоваться (полчаса, шаблон типовой инструкции приведен в конце статьи)

Пошаговая инструкция что как делать:

Шаг 1, добавляем в проект скрипты для подготовки установочного пакета и деплоя:

Создаем в корне своего проекта каталог CI, скачиваем в него два скрипта: deploy.sh и build-inst.sh вот отсюда https://github.com/tomsksoft-llc/ci-deploy-scripts-bash.git

Не нравится имя каталога CI? Используйте любое другое, только учитывайте это при дальнейшей настройке.

Шаг 2, настраиваем скрипты под наш проект:

build-inst.sh – скрипт создания установочного пакета, запускается только на билд-сервере, на удаленных серверах не нужен.

В самом верху скрипта строчка:

INSTALL_PCKG_SUFFIX="my_project"

Меняем «my_project» на что подходит для вашего проекта. Это будет использовано как часть имени файла установочного пакета.

Дальше ищем:

tar -czf "${BUILD_VERSION}_${INSTALL_PCKG_SUFFIX}.tgz" --exclude="CI" ./* 

Создание пакета – это просто архивирование всех нужных файлов в один файл, при этом исключаются не нужные, в данном случае каталог с нашими скриптами. Что бы исключить еще что-то, просто пишем следующий –exclude=”bla…..” и так сколько надо. Внимание! Некоторые версии tar имеют чуть другой синтаксис, проверьте что на вашем билд-сервере оно работает как вам надо. Этот скрипт будет запускаться только там и нигде более.
Можете переписать скрипт как угодно вам, интегрировать его в ваш процесс сборки, или просто взять из него одну строчку для получения архива – как вам удобнее.
Задача – получить файл архива, который можно перенести на удаленный сервер, развернуть и запустить из полученных файлов ваш проект.

deploy.sh – он выполняет все функции, перечисленные в начале статьи, работает на удаленных серверах.

Верхняя часть файла, константы, задаем их:

OWNER="myusername"

Здесь надо указать имя того пользователя, под которым ваш CI будет заходить на удаленные сервера по ssh.

TARGET_DIR_STATE="drwxr-xr-x${OWNER}mygroupname"

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

BUILDS_DIR_STATE="drwxr-xr-x${OWNER}mygroupname"

Все тоже самое, но для каталога, который находится уже внутри предыдущего. В этом каталоге хранятся версии вашего проекта. Обычно все должно совпадать с предыдущим каталогом, но мало ли…. Кстати, он называется revs. Если хотите поменять это имя – меняйте, в предыдущей строке:

BUILDS_DIR="revs"

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

SITE_ROOT_SYMLINK="site" 
SITE_ROOT_SYMLINK_STATE="lrwxrwxrwx${OWNER}mygroupname"

Это название и права доступа к файлу сим-линку, который будет указывать на текущую рабочую версию. Именно имя этого сим-линка вы должны использовать в качестве «doc_root» при настройке веб сервера. Здесь также задайте имя группы, которой будет принадлежать файл, и если нужно поменяйте его имя и права доступа.

Шаг 3, подготовка структуры каталогов на сервере:

На сервере(ах) куда будем деплоить нужен локальный пользователь и группа, которые мы задали в конфигурации на предыдущем шаге.
Создаем каталог в который будем все заливать-деплоить, назначаем ему владельцев и права которые задали в конфигурации.
Внутри этого каталога создаем подкаталог revs (или как вы его назвали на предыдущем шаге) и назначаем ему владельцев и права которые указали в конфигурации.
Создаем файл сим-линк (указывающий куда угодно, хоть на несуществующий файл), назначаем ему владельца и права, которые задали в конфигурации.
Если у нас уже развернута живая система на этом сервере, то, есть хитрость легко переехать на данную систему без даунтайма. Но об этом позже, давайте сначала убедимся, что у нас все заработало.

Вот пример как можно это сделать:

$ mkdir myproject
$ chmod 755 myproject/
$ mkdir myproject/revs
$ chmod 755 myproject/revs/
$ ln -s foo myproject/site

Вот что примерно получится:

drwxr-xr-x 3 myuser mygroup 4096 Sep 19 11:51 myproject:
  drwxr-xr-x 2 myuser mygroup  4096 Sep 19 11:51 revs
  lrwxrwxrwx 1 myuser mygroup 3 Sep 19 11:51 site -> foo

Шаг 4, приступаем к использованию, тонкая настройка

Давайте попробуем получить установочный пакет, для этого переходим в корень репозитария нашего проекта и запускаем:

$ ./CI/build-inst.sh 1
Try to build for 1:
OK: Build successfully.

В результате получим файл 1_myproject.tgz, в текущем каталоге.

Вот так его можно задеплоить на наш удаленный сервер (где мы уже настроили инфраструктуру):

$ scp CI/deploy.sh myuser@myserver:~
$ scp 1_myproject.tgz myuser@myserver:/home/myuser/myproject/revs/
$ ssh myuser@myserver bash ~/deploy.sh /home/myuser/myproject /home/myuser/myproject/revs/1_myproject.tgz

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

Я здесь не останавливаюсь на том, как обеспечить доступ по ssh, но проще всего сделать это с помощью RSA ключей. На билд сервере будет лежать закрытая часть ключа, а на удаленном, открытая в каталоге пользователя, под которым мы идем (тогда для команд scp и ssh надо будет указать ключ: -i с именем файла ключа в качестве первого аргумента команды).
Инструкции как это сделать можно легко найти в интернете.

Если у вас получилось, то скрипт выполнится успешно, и на удаленном сервере вы увидите такое состояние каталогов:

myproject:
lrwxrwxrwx  1 myuser mygroup   23 Sep 19 12:16 backup1 -> /home/myuser/myproject/foo
drwxr-xr-x  3 myuser mygroup 4096 Sep 19 12:16 revs
lrwxrwxrwx  1 myuser mygroup   36 Sep 19 12:16 site -> /home/myuser/myproject/revs/1_myproject

В каталоге /home/myuser/myproject/revs/1_myproject будет развернуто содержимое файла архива, который мы передали в качестве инсталляционного пакета.
При этом сам файл архива после успешного деплоя удаляется.

В процессе выполнения скрипта он выведет кучу диагностики – это нормально. В случае, если что-то пошло не так, скорее всего она вам поможет найти ошибку. Если же все было ОК, то завершающий вывод будет примерно таким:

OK...............: Deploy script deploy.sh
    on myserver
    for /home/myuser/myproject
    from  /home/myuser/myproject/revs/1_myproject
    successfully done.

Теперь надо учесть внешние зависимости нашего проекта. Это могут быть постоянные файлы/каталоги, которые должны сохранятся от деплоя к деплою (простейший пример – конфигурационные файлы и файлы локальных настроек).
Принцип похож на тот, что используется в докере – мы просто храним такие каталоги и файлы локально на удаленном сервере в отдельном месте выше каталога проекта (можно и даже нужно их хранить в каталоге куда мы выполняем деплой, т.е. в нашем случае в /home/myuser/myproject, но это не обязательно, делайте как вам удобнее), а при деплое, линкуем их в нужные места в каталоге где развернута конкретная версия (если таких зависимостей в вашем проекте нет, то переходите сразу к пункту «Правила использования скрипта deploy.sh»).

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

Функция deploy_project_environment() – именно сюда добавляем действия, которые надо выполнить между тем моментом, когда архив уже развернут (т.е. все новые файлы на месте) и тем, когда рабочий сим-линк будет переключен на каталог с новой версией. Если вы вернете из э той функции НЕ ноль, то деплой будет остановлен, и переключения на новую версию не произойдет (старая версия останется работать как ни в чем не бывало).
Например там может быть создание линка внутри только что развернутой версии проекта на файл или каталог который лежит выше и является перманентным (сохраняется между сменами версий). Можно сделать это так:

if ! create_symlink "$_target_dir/.env" "$_build_dir/.env"
then
    return 1
fi

Функция create_symlink объявлена внутри нашего скрипта, можете использовать ее просто для удобства, она работает сразу и с каталогами и с файлами и выводит стандартное диагностическое сообщение.
Этот и еще один пример в закомментированном виде есть в самом скрипте, пользуйтесь. Ну и вообще, делайте что нужно, а в случае ошибки возвращайте НЕ ноль чтобы прервать деплой в тот момент, когда на живой системе еще нет никаких изменений.

Функция test_env_cons() – сюда можно вставить любые проверки, они будут выполнены еще до попытки распаковать файл. Если функция возвращает НЕ ноль, то деплой будет остановлен. В этой функции уже есть три проверки – на существование и правильность прав доступа к основному каталогу, каталогу с ревизиями и сим-линку на рабочую версию. Можете расширять по образу и подобию своими проверками, закомментированный пример есть в коде скрипта. Функция test_dst, которая используется в примерах, определена внутри скрипта, можете пользоваться ей просто для удобства.

Функция test_content() – она ищет файлы, которые были изменены в рабочей версии уже после того, как она стала основной рабочей. Для того, чтобы избежать реакции на файлы или имена каталоги, которые могут меняться исключите их таким образом:

ищем строчку

if [ "$(find "$link_src" -newer "$link" | wc -l)" -gt 0  ]

и для исключения, например, файла .env, добавляем его вот таким образом:

if [ "$(find "$link_src" -newer "$link" | grep -v .env | wc -l)" -gt 0  ]

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

find "$link_src" -newer "$link" | grep -v .env

Правила использования скрипта deploy.sh

Получить справку по использованию можно запустив скрипт без аргументов:

deploy.sh [-f|--force] TARGET_DIR   BUILD      - to deploy BUILD into TARGET_DIR
TARGET_DIR - directory where all environment will be deployed.
BUILD can be .tgz archive or a directory within TARGET_DIR/revs.
force: Deploy anyway (the script will delete existing directory and ignore local changes).

deploy.sh [-t|--test] TARGET_DIR             - to test environment consistent in TARGET_DIR
deploy.sh [-r|--restore] TARGET_DIR          - to restore live from backup1 link if it exist in TARGET_DIR
deploy.sh [-с|--content] TARGET_DIR          - to check if any file(s) was changed after deploy in TARGET_DIR
deploy.sh [-s|--scavenge] TARGET_DIR         - delete old not used revision in TARGET_DIR
deploy.sh [-h|--help]                        - to print this message

Основная функция - это деплой, ее синтаксис указан в самом начале описания. Немножко пояснений по поводу флажка force – по умолчанию скрипт если наткнется на любую проблему сразу же прервет деплой (например, уже существует версия с таким же именем как вы хотите задеплоить, или в текущей активной ревизии есть файлы с локальными изменениями). Если вы укажете ключ -f, то такие проблемы будут проигнорированы, но резервная копия при этом все равно будет создана (путем переименования старого каталога).
Функция очистки -s удаляет только те ревизии, которые не являются бекапной копией или текущей рабочей системой, и при этом каталог создан не менее чем за 5 дней до момента на который производится очистка.
Функция restore возвращает систему на одну версию обратно, т.е. текущая рабочая версия заменяется версией из backup1, backup1 заменяется версией из backup2 и т.д. Всего хранится три бекапных линка, но если надо больше, то просто поменяйте код скрипта в функции link_to_site() (увеличьте счетчик цикла, который создает бекапные линки до нужного вам значения).

Отлаживаем и запускаем

Остается теперь перенастроить ваш сервер так, чтобы он брал ваш сайт по нашему сим-линку site. Например, для веб серверов достаточно просто указать его в качестве doc_root. Бывают специфические настройки, которые влияют на поведение веб серверов при таком деплое, например, для ngnix желательно использовать директиву $realpath_root.

Проще и надежнее всего поднимать новую систему рядом с уже существующей, с другим именем хоста, там все отлаживать и проверять, и уже потом менять имя хоста в конфигурационном файле веб-сервера.
И только если вам позволяет опыт, цена ошибки не высока и вообще, все звезды сложились, то можно попробовать сделать так: укажите текущему сим-линку целевой каталог где на данный момент находится ваша система, перенастройте сервер и выполните рестарт. Дальше разберитесь с внешними зависимостями, например, все что нужно оставьте в каталоге вашей текущей системы (ведь она сейчас работает), настройте скрип деплоя чтобы линки делались туда, и пробуйте деплой. Если с первого раза не получилось, просто выполняйте restore, он будет возвращать сим-линк на ваш прежний каталог.

Шаг 5, как это интегрировать в инфраструктуру проекта и его workflow:

Для интеграции деплоя в ваш CI, нужно просто вставить запуск наших скриптов в нужные места. Тут все зависит от вашего workflow и инфраструктуры. Например, для формирования инсталляционного пакета используете команду:

./CI/build-inst.sh <$Version_id>

Где в качестве Version_id можете использовать номер сборки, хеш ревизии из git, или их комбинацию. Это позволит кроме всего прочего легко идентифицировать какая ревизия залита на вашу систему (конкретный сервер).
Не забывайте в CI обрабатывать код возврата. Скрипты в случае ошибки возвращают НЕ ноль, реагируйте на это (хотя, например, Jenkins и Gitlab CI отреагируют на это сами и прервут выполнение своей Job-ы).

Для деплоя используйте комбинацию команд:

$ scp -i id_rsa_file_name CI/deploy.sh myuser@myserver:~
$ scp -i id_rsa_file_name <install_pkg_name>.tgz myuser@myserver:/home/myuser/revs/
$ ssh -i id_rsa_file_name myuser@myserver bash ~/deploy.sh /home/myuser/myproject /home/myuser/myproject/revs/<install_pkg_name>.tgz

Если серверов несколько, то сначала копируйте инсталляционный пакет на все, и если все получилось, только тогда на каждом из них, последовательно, запускайте скрипт деплоя.
Если на одном сервере несколько проектов, то просто переименуйте скрипт deploy.sh прямо в репозитарии проекта (добавьте к имени файла идентификатор проекта) и тогда вы избежите перетирания файлов при деплое разных проектов на один сервер.
Если после деплоя нужны дополнительные действия для вступления изменений в силу – вставляйте эти команды сразу после скрипта деплоя (не забывая обрабатывать его код возврата!). Это могут быть, например, рестарт php-fpm, рестарт вашего сервиса и т.д. и т.п.

Кроме этого, можно еще по крону раз в день запускать на билд-сервере скрипт примерно такого содержания:

$ ssh -i id_rsa_file_name myuser@myserver "/bin/bash ~/deploy.sh -t /home/myuser/myproject"
$ ssh -i id_rsa_file_name myuser@myserver "/bin/bash ~/deploy.sh -c /home/myuser/myproject"
$ ssh -i id_rsa_file_name myuser@myserver "/bin/bash ~/deploy.sh -s /home/myuser/myproject"

Выводить это в лог и отслеживая код завершения каждой из команд, например, в случае ошибки присылать на почту.
Такой подход позволит вам обнаружить ошибки в инфраструктуре сразу в день их появления (иначе, можно обнаружить ее через полгода, когда срочно надо залить какое-то изменение в проект, а тут выясняется, что у вашего пользователя больше нет доступа к серверу…).
Хотя это на ваше усмотрение, мы такой подход используем и он себя оправдывает (например, смена IP адреса на билд-сервере, в результате чего тот теряет доступ к серверам, ну потом что там настроен firewall, и не пускает кого попало – вы об этом узнаете в тот же день и исправляете по горячим следам).

Шаг 6, пишем инструкцию по использованию для разработчиков на основании шаблона:

Просто приведу пример такой инструкции, основано на одном из наши workflow для конкретной группы проектов. Если будете делать такую инструкцию для ваших инженеров по данному шаблону, это займет несколько минут 30 (вместе с перерывом на чай/кофе):

Если решите использовать данные скрипты, помните – мы не несем ответственности за последствия их применения в первозданном или измененном виде.

Если решились использовать – удачи вам, и пожалуйста, поделитесь с нами своим успешным опытом!