Yesod: разворачиваем приложение без Keter

Приветствую!

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

Развернуть Yesod-приложение можно двумя базовыми способами: с использованием Keter и без него. И хотя автор Yesod позиционирует Keter как умолчальный инструмент для разворачивания, в этой заметке мы рассмотрим “сырое” разворачивание, без Keter или каких-либо иных сторонних средств.

Описываемый ниже способ предельно прост, прозрачен, однако не лишён недостатков.

Сценарий

Как вы уже знаете из предыдущих заметок, после сборки нашего Yesod-проекта в каталоге dist/build/webhs/ появился исполняемый файл webhs. Этот файл удобен тем, что не тянет за собой никаких специфических Haskell-зависимостей, поэтому, будучи собранным на одной системе, он может быть запущен на другой такой же системе. А это именно то, что нам нужно!

Далее подразумевается следующий сценарий (кстати, рекомендуемый автором Yesod):

  1. Есть разработческий сервер, на котором установлена Haskell Platform и Yesod.
  2. Есть рабочий сервер, на котором есть только необходимая для работы приложения инфраструктура (веб-сервер и СУБД).

В моём случае оба эти сервера работают под Ubuntu 14.04.1, а на рабочем сервере установлен Nginx и PostgreSQL.

Подготовка рабочего сервера

Создаём рабочий каталог, в котором будет жить наше приложение. Скажем, /srv/www/webhs. В нём создаём ещё три каталога:

/srv/www/webhs/
    bin/
    config/
    static/

Каталог bin предназначен для исполняемого файла webhs, каталог config - это копия каталога config в корне нашего проекта, а каталог static - это копия каталога static там же.

Теперь по поводу веб-сервера. Тут всё просто: классическая модель перенаправления. Например, в моём случае (а возможно, и в вашем) приложение представлено четырьмя веб-стендами, поэтому получается нечто подобное:

  1. Запросы, поступающие на http://alpha.my-service.com, перенаправляются на localhost:3001.
  2. Запросы, поступающие на http://beta.my-service.com, перенаправляются на localhost:3002.
  3. Запросы, поступающие на http://rc.my-service.com, перенаправляются на localhost:3003.
  4. Запросы, поступающие на http://my-service.com, перенаправляются на localhost:3004.

Соответственно, порты с 3001 по 3004 - это те самые порты, которые заданы в файле config/settings.yml. Именно их будут слушать четыре версии нашего приложения.

Ну и по поводу Postgres всё совсем просто: создаём пользователя и БД, соответствующие указанным в файле config/postgresql.yml. Рассказывать о том, как это сделать, я не стану, ведь если вы уже выбрали PostgreSQL, значит вы уже знаете как с ним работать, не так ли?

Стенды

Как уже было упомянуто, наше приложение представлено четырьмя версиями, работающими на четырёх стендах (поддоменах). Для удобства можно расширить рабочий каталог, предоставив для каждой из версий свою квартирку:

/srv/www/webhs/
    alpha/
        bin/
        config/
        static/
    beta/
        bin/
        config/
        static/
    rc/
        bin/
        config/
        static/
    stable/
        bin/
        config/
        static/

Полагаю, тут всё понятно без комментариев.

Вперёд!

После того, как мы запушили изменения в соответствующую версионную ветку, запускается наш CI-механизм (ведь у вас есть такой, не правда ли?). В моём случае это рассмотренный ранее Buildbot. Он, увидев изменения в ветке, дёргает скрипт в корне репозитория проекта. Пусть в нашем случае это будет webhs_update.sh. Внутри этого скрипта совершаются следующие шаги:

  1. Взятие исходников проекта из соответствующей версионной ветки.
  2. Сборка проекта.
  3. Копирование каталога static в уже известное нам место на рабочем сервере.
  4. Остановка старого процесса нашего приложения на рабочем сервере.
  5. Удаление старого исполняемого файла нашего приложения на рабочем сервере.
  6. Копирование нового исполняемого файла на рабочий сервер.
  7. Старт нового процесса нашего приложения на рабочем сервере.

Рассмотрим эти шаги на примере разворачивания версии alpha. Вот каким может быть такой скрипт:

#!/bin/bash

git checkout --force alpha && git pull origin alpha
cabal clean && cabal configure && cabal build
scp -r ~/webhs/static/* [email protected]:/srv/www/webhs/alpha/static/
ssh [email protected] "./webhs_ctl.sh alpha stop"
ssh [email protected] "rm -f /srv/www/webhs/alpha/bin/webhs-alpha 2> /dev/null"
scp ~/webhs/dist/build/webhs/webhs [email protected]:/srv/www/webhs/alpha/bin/webhs-alpha
ssh [email protected] "./webhs_ctl.sh alpha start"

Пара пояснений.

Во-первых, напомню, что такого вида строка:

ssh [email protected] "COMMAND"

запускает команду COMMAND на рабочем сервере в домашнем каталоге пользователя denis. Во-вторых, подразумевается, что у вас уже есть ssh-ключ, дающий право на ssh-копирование с разработческого сервера на рабочий. А в-третьих, здесь упоминается скрипт webhs_ctl.sh, уже лежащий в вашем домашнем каталоге на рабочем сервере. Пара слов об этом.

Управляющий скрипт

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

$ touch webhs_ctl.sh
$ chmod +x webhs_ctl.sh

Этот скрипт может принимать два аргумента: имя стенда (alpha, beta, rc и stable) и команду (stop, start и restart). Поэтому для запуска процесса на alpha-стенде мы делаем так:

$ ./webhs_ctl.sh alpha start

Соответственно, внутри этого скрипта, после разбора аргументов, осуществляется либо запуск процесса нашего приложения, либо его останов. С остановом всё просто, можно использовать команду killall с передачей ему имени нашего исполняемого файла. А вот с запуском чуток посложнее.

Демонизация

Разумеется, наш процесс должен быть демоном. Следовательно, мы не можем запустить наше приложение просто так:

$ cd /srv/www/webhs/alpha
$ bin/webhs-alpha Development

Нужно как-то демонизировать наш процесс, дабы отвязать его от текущего терминала. Сделать это можно по-разному, например через команду start-stop-daemon или nohup. Я выбрал nohup, и после долгих исканий по Сети я сформировал такой вариант:

$ nohup bin/webhs-alpha Development > /dev/null 2>&1 &

Такая команда запускает процесс и полноценно отвязывает его от текущего терминала, причём сама эта команда правильно завершается (что чрезвычайно важно, если мы хотим запихнуть её в скрипт). Кстати, я напоминаю, что запускать процесс нужно не из каталога bin, а из каталога уровнем выше, то есть из той точки, в которой лежит каталог config.

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

Вот и всё

Ну что ж, о “сыром” разворачивании без Keter достаточно. Плюс такого способа в том, что он предельно прост и совершенно прозрачен. Однако есть и существенный минус: если процесс нашего приложения (по тем или иным причинам) умрёт, некому будет его воскресить. А у Keter, скажу забегая вперёд, есть автоматический мониторинг за процессом, что очень полезно для production-среды. Поэтому о Keter обязательно поговорим в одной из следующих заметок.