Космическая Змея в Магазине или Как Мы «CheeseShop» Ставили

в 10:55, , рубрики: deployment, django, python, rpm, системное администрирование, метки: , ,

Доброе время суток, уважаемые читатели!

Ниже приведена увлекательная(?) история о том как наша организация решала проблему т.н. «деплоймента как у людей». Наш основной язык разработки Python, с примесями разных интересных (и не очень) пакетов (Django, Bottle, Flask, PIL, ZMQ, и т.д.).

Начнём с краткого описания одного из наших приложений:

  • Django 1.4
  • MySQL
  • Celery для крон-имитации и поддержки вспомогательных функций в фоновом режиме
  • Daemon-процесс, основанный на Django management command

Всё это дело работает под связкой gUnicorn и nginx, на ОС CentOS 5.8.

Детали, как принято, ниже.

Суть Проблемы

В одной из заключительных фаз проекта, нас посетила мысль о том, что в принципе, «svn up && python manage.py syncdb && python manage.py migrate» это криво; начались поиски «более оптимального» подхода.

Вариант Первый — «Змея в Космосе»

Так-как мы используем Spacewalk для менеджмента серверов, возникла идея паковать наше приложение в RPM-пакет; возможность установки «одним кликом» манила и вариант был принят в разработку.

Часов через 8, когда показатель WTF/hr застрял в красной зоне, мы решили искать что-нибудь попроще. Основные причины:

  • Все зависимости надо паковать в RPM
  • Не всем пакетам/дистрибутивам это «нравится».
  • Процесс упаковки ведёт к fork-у пакета, т.к. часто надо редактировать setup.py для правильной конвертации в .spec-файл.
  • Сама среда упаковки требует относительно много опыта.

Вариант Второй — «Змея в Магазине»

Второй вариант возник после слов, «а как же мы ставим чужие пакеты?» — и поиски локальной копии PyPi начались. Из множества заброшенных и безперспективных пакетов, был выбран localshop, который порадовал простотой установки и даже не просил странных пакетов именно ВОТ ТОЙ версии.

Подгон нашего приложения занял относительно немного времени — всё что нам требовалось это добавить setup.py и указать «левые» пакеты прямо там, хотя на грабли мы все-таки наступили:

Надо было явно указать zip_safe=False и include_package_data=True, т.к. некоторые файлы не распаковывались при установке.

Apache (который скоро будет заменен) надо было указать KeepAliveTimeout 300, SetEnv proxy-sendcl и ProxyTimeout 1800 для загрузки крупных пакетов.

Кроме этого, процесс конфигурации localshop-а прошел нормально, достаточно было запустить (под «чистой» учеткой):


cd && virtualenv venv && . venv/bin/activate && pip install localshop
~/venv/bin/localshop init && ~/venv/bin/localshop upgrade

После чего нам осталось лишь подогнать ~/.pipyrc под наш «магазин»:

[distutils]
index-servers = local

[local]
username: developer
password: parolcheg123
repository: http://cheese.example.com/simple/

После чего, процесс релиза сводится к python setup.py upload -r local, после изменения номера версии в setup.py.

Финальный Подход

При первой попытке установить наше приложение на «боевом» сервере, нас постигла грустная участь — нам требовался пакет PIL, а GCC и разные штуки типа libpng-devel отсуствовали как класс. Пришлось все-таки «собрать ручками» RPM-пакет Питона и разных интересных штук (MySQL-python, setuptools, PIL, ZMQ) и залить на Spacewalk.

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

  • Авто-запуск gUnicorn (для localshop вообще и наше приложения конкретно): хватило мелкой обработки напильником откопанного скрипта.
  • «Правильная» настройка pip на «боевых» серверах: в раздел [global] в (новом) файле ~produser/.pip/pip.conf добавить index-url = cheese.example.com/simple/
  • Добавка всего этого добра в систему мониторинга (OpsView): добавка нового service check-а на процесс с аргументами «run_gunicorn» или «gunicorn» и привязка к серверу.

Кроме того, нам понадобилось запустить «долгоиграющий» процесс, а также Celery. Для нашего приложения решено было использовать ZDaemon, для которого были написаны init-script и файл настройки:

#!/bin/bash
### BEGIN INIT INFO
# Provides:              our_app
# Required-Start:        $all

# Required-Stop:         $all
# Default-Start:         2 3 4 5
# Default-Stop:          0 1 6
# Short-Description:     controls our_app via zdaemon
# Description:           controls our_app via zdaemon

### END INIT INFO

. /etc/rc.d/init.d/functions

. /etc/sysconfig/network

. ~produser/venv/bin/activate

# Check that networking is up.
[ "$NETWORKING" = "no" ] && exit 0


RETVAL=0
APP_PATH=~/produser/app/
PYTHON=~produser/venv/bin/python
USER=produser



start() {
        cd $APP_PATH
        zdaemon -C our_app.zdconf start
        zdaemon -C our_app_celery.zdconf start

}

stop() {
        cd $APP_PATH
        zdaemon -C our_app.zdconf stop
        zdaemon -C our_app_celery.zdconf stop

}

check_status() {
        cd $APP_PATH
        zdaemon -C our_app.zdconf status
        zdaemon -C our_app_celery.zdconf status

}

case "$1" in
  start)
        start
        ;;
  stop)
        stop
        ;;
  status)
        check_status
        ;;
  restart)
        stop
        start
        ;;
  *)
esac
exit 0

# our_app[_celery].zdconf
<runner>
        daemon true
        directory /opt/produser/app/
        forever false

        backoff-limit 10
        user produser
# run_command -> actual command or celeryd
        program /opt/produser/venv/bin/python /opt/produser/app/manage.py run_command --settings=prod_settings
        socket-name /tmp/our_app.zdsock
</runner>

Конечный результат

Запуск ~/venv/bin/activate && pip install -U our_app под учеткой produser почти без проблем устанавливает свежую версию нашего приложения + все «левые» пакеты указанные в setup.py.

Процесс syncdb и migrate все-таки производится «ручками», но:

  • «Боевая» версия всегда известна
  • Нет надобности ставить GCC и т.д. на «боевые» сервера
  • Rollback прост

Надеюсь что сие описание процесса просветило кого-то из читателей.

Автор: marklarius

* - обязательные к заполнению поля