19 июля 2010 г.

musicmans.ru | Как сделать сайт на Django | Развертывание

Я обещал выкладывать все этапы работы на http://musicmans.ru, поэтому настала пора вывесить табличку "Сайт в разработке" :), заодно наладив работу развертывания.

Итак, задачи: создать приложение по вводу сайта в режим обслуживания, настроить сервер, автоматизировать процесс развертки на сервер с помощью fabric.

Вспомним о том, что у нас есть redmine и mylyn, создадим данные задачи (не забываем создать категории задач в настройках проекта в redmine).

django-maintenancemode

Для ввода сайта в режим обслуживания есть целое приложение.

Устанавливаем:
  1. C:\>c:\Python26\Scripts\pip.exe install django-maintenancemode  
  2. Downloading/unpacking django-maintenancemode  
  3.   Downloading django-maintenancemode-0.9.2.tar.gz  
  4.   Running setup.py egg_info for package django-maintenancemode  
  5. Installing collected packages: django-maintenancemode  
  6.   Running setup.py install for django-maintenancemode  
  7. Successfully installed django-maintenancemode  
  8. Cleaning up...  

Прописываем в requirements.txt:

django-maintenancemode==0.9.2

Настраиваем. В MIDDLEWARE_CLASSES добавляем "maintenancemode.middleware.MaintenanceModeMiddleware".



В templates создаем файл 503.html со статическим содержимым того, что будет выводиться в период обслуживания сайта.

Функции приложения:
* MAINTENANCE_MODE - включает\выключает режим обслуживания, по умолчанию: False.
* Страница 503 не отображается залогиненым админам и клиентам с ip адресами, входящими в INTERNAL_IPS.

Итак, пропишем MAINTENANCE_MODE = True, в development.py и в production.py (в development.py закомментируем вскоре).

Запускаем pydev сервер, отладку, переходим на страницу и видим следующее:



Немного поправим 503.html по своему желанию.

Настройка сервера

Устанавливаем и настраиваем фаерволл:
  1. $ sudo aptitude install ufw  
  2. $ sudo ufw enable  
  3. $ sudo ufw logging on  
  4. $ sudo ufw allow 80/tcp  
  5. $ sudo ufw allow SSH_port  
  6. $ sudo ufw default deny  

Настройку веб сервера выбрал такую (nginx + uwsgi). Тем более, nginx, начиная с версии 0.8.40 поддерживает uwsgi из коробки.
  1. # apt-get install gcc libssl-dev libpcre++-dev make  
  2. # wget http://sysoev.ru/nginx/nginx-0.8.44.tar.gz  
  3. # tar -xzvf nginx-0.8.44.tar.gz  
  4. # cd nginx-0.8.44/  
  5. # ./configure --conf-path=/etc/nginx/nginx.conf \  
  6.   --prefix=/usr \  
  7.   --error-log-path=/var/log/nginx/error.log \  
  8.   --pid-path=/var/run/nginx.pid \  
  9.   --lock-path=/var/lock/nginx.lock \  
  10.   --http-log-path=/var/log/nginx/access.log \  
  11.   --with-http_dav_module \  
  12.   --http-client-body-temp-path=/var/lib/nginx/body \  
  13.   --with-http_ssl_module \  
  14.   --http-proxy-temp-path=/var/lib/nginx/proxy \  
  15.   --with-http_stub_status_module \  
  16.   --http-fastcgi-temp-path=/var/lib/nginx/fastcgi \  
  17.   --http-uwsgi-temp-path=/var/lib/nginx/uwsgi \  
  18.   --http-scgi-temp-path=/var/lib/nginx/scgi \  
  19.   --with-debug \  
  20.   --with-http_flv_module   
  21. # make  
  22. # make install  

Скрипт запуска /etc/init.d/nginx я взял из стандартного пакета debian (устанавливать его не нужно, ибо можно перетереть новые конфиги старыми. В принципе, не страшно, так как мы будем их писать заново, но например mime.types могут отличаться).

Создаем рабочую директорию для сайта, например /srv/musicmans
Структура:
  1. /srv/musicmans   
  2. |  backups  
  3. --|  src  
  4. --|  db  
  5. |  logs  
  6. |  root  
  7. --|  src  
  8. --|  www  

На машине разработчика создаем файл в src wsgi.py (основой файл запуска проекта для веб-сервера):

  1. import os  
  2. import sys  
  3. import locale  
  4. import django.core.handlers.wsgi  
  5.   
  6. DIR=(os.path.abspath(__file__))  
  7. sys.path.append(DIR)  
  8. os.environ['DJANGO_SETTINGS_MODULE'] = 'settings.production'  
  9.   
  10. def force_utf8_hack():  
  11.   reload(sys)  
  12.   sys.setdefaultencoding('utf-8')  
  13.   for attr in dir(locale):  
  14.     if attr[0:3] != 'LC_':  
  15.       continue  
  16.     aref = getattr(locale, attr)  
  17.     locale.setlocale(aref, '')  
  18.     (lang, enc) = locale.getlocale(aref)  
  19.     if lang != None:  
  20.       try:  
  21.         locale.setlocale(aref, (lang, 'UTF-8'))  
  22.       except:  
  23.         os.environ[attr] = lang + '.UTF-8'  
  24.   
  25. force_utf8_hack()  
  26.   
  27.   
  28. application = django.core.handlers.wsgi.WSGIHandler()  

Перед тем, как настраивать сервер, запустим тестирование проекта.



Введем команду test и получим ошибку.

Добавим в development.py:
  1. INSTALLED_APPS += (  
  2.                    'django.contrib.admin',  
  3.                    )  

А также в manage.py:
  1. if settings.DEBUG and command == "test":  
  2.     settings.MAINTENANCE_MODE = False  
  3.   
  4. execute_manager(settings)  

Ибо нам тесты в режиме обслуживания не нужны, да и не отрабатывают они, у меня вышла ошибка отсутствия темплейта 503.html и куча других.

Сделаем коммит.

Вернемся к серверу с сайтом. Сделаем предварительную настройку:

1. Перейдем в директорию /srv/musicmans/ и заберем транк в root:

$export SVN_SSH="ssh -l loginname"

или сделаем пару ключа (она нам все рано пригодиться при использовании fabric). На сервере с сайтом:
  1. $ ssh-keygen -t dsa  
  2. $ cat ~/.ssh/id_dsa.pub  

копируем вывод, добавляем на сервер с кодом в ~/.ssh/authorized_keys2 на сервер (если файла нет, то touch ~/.ssh/authorized_keys2 && chmod 600 ~/.ssh/authorized_keys2 ). Пробуем логиниться без пароля.
  1. $svn checkout --depth=empty svn+ssh://codesrv/repos/musicmans/trunk/backend root  
  2. $cd root/  
  3. $svn update --set-depth=infinity www  
  4. $svn update --set-depth=infinity src  

Так как нам нужны только две директории src и www, то делаем пустой checkout, после чего обновляем две директории с бесконечной вложенностью. После этого svn update будет нам обновлять только директории www и src.

Устанавливаем необходимые приложения для сайта:
  1. vermus@musicmans:~$ cd /srv/musicmans/root/src  
  2. vermus@musicmans:~$ sudo pip install -r requirements.txt --download-cache /usr/src/pipcache/  

Установка postgresql:
  1. # apt-get install postgresql python-psycopg2  
  2. # su postgres  
  3. $ createuser musicmans --no-superuser --no-createdb --no-createrole --login --pwprompt --encrypted  
  4. $ createdb --owner=musicmans --encoding=utf-8 musicmans  

База создана, пробуем синхронизировать django с базой данных (мы это делали уже на машине разработчика, но так как у нас база на сайте будет жить своей жизнью, а девелоперская своей, то сделаем это еще раз, т.е. миграцию данных выполнять не будем):
  1. vermus@musicmans:~$ cd /srv/musicmans/root/src/  
  2. vermus@musicmans:/srv/musicmans/root/src$ python manage.py syncdb  

Итак, все в порядке. Осталось настроить веб-сервер. Конфигурацию мы уже выбрали.

Установим uwsgi сервер:
  1. $ cd /usr/src/  
  2. $ sudo pip install http://projects.unbit.it/downloads/uwsgi-latest.tar.gz  

Настроим скрипт init.d для запуска через файловый сокет сервера uwsgi с проектом (/etc/init.d/uwsgi):
  1. # cat uwsgi  
  2. ### BEGIN INIT INFO  
  3. # Provides:          uwsgi  
  4. # Required-Start:    $all  
  5. # Required-Stop:     $all  
  6. # Default-Start:     2 3 4 5  
  7. # Default-Stop:      0 1 6  
  8. # Short-Description: starts the uwsgi app server  
  9. # Description:       starts uwsgi app server using start-stop-daemon  
  10. ### END INIT INFO  
  11.   
  12. PATH=/sbin:/bin:/usr/sbin:/usr/bin  
  13. DAEMON=/usr/bin/uwsgi  
  14.   
  15. OWNER=uwsgirun  
  16.   
  17. NAME=uwsgi  
  18. DESC=uwsgi  
  19.   
  20. test -x $DAEMON || exit 0  
  21.   
  22. # Include uwsgi defaults if available  
  23. if [ -f /etc/uwsgi ] ; then  
  24.         . /etc/uwsgi  
  25. fi  
  26.   
  27. set -e  
  28.   
  29. DAEMON_OPTS="--socket /var/lib/nginx/uwsgi/musicmans.sock --chmod-socket -d /srv/musicmans/logs/uwsgi.log --pythonpath $PYTHONPATH --module $MODULE"  
  30.   
  31. case "$1" in  
  32.   start)  
  33.         echo -n "Starting $DESC: "  
  34.         start-stop-daemon --start --chuid $OWNER:$OWNER --user $OWNER \  
  35.                 --exec $DAEMON -- $DAEMON_OPTS  
  36.         echo "$NAME."  
  37.         ;;  
  38.   stop)  
  39.         echo -n "Stopping $DESC: "  
  40.         start-stop-daemon --signal 3 --user $OWNER --quiet --retry 2 --stop \  
  41.                 --exec $DAEMON  
  42.         echo "$NAME."  
  43.         ;;  
  44.   reload)  
  45.         killall -1 $DAEMON  
  46.         ;;  
  47.   force-reload)  
  48.         killall -15 $DAEMON  
  49.        ;;  
  50.   restart)  
  51.         echo -n "Restarting $DESC: "  
  52.         start-stop-daemon --signal 3 --user $OWNER --quiet --retry 2 --stop \  
  53.                 --exec $DAEMON  
  54.         sleep 1  
  55.         start-stop-daemon --user $OWNER --start --quiet --chuid $OWNER:$OWNER \  
  56.                --exec $DAEMON -- $DAEMON_OPTS  
  57.         echo "$NAME."  
  58.         ;;  
  59.   status)  
  60.         killall -10 $DAEMON  
  61.         ;;  
  62.       *)  
  63.             N=/etc/init.d/$NAME  
  64.             echo "Usage: $N {start|stop|restart|reload|force-reload|status}" >&2  
  65.             exit 1  
  66.             ;;  
  67.     esac  
  68.     exit 0  

Не забываем создать пользователя uwsgirun, под которым будет запускаться uwsgi. Параметр chmod-socket устанавливает права 666 на сокет, если Вас это не устраивает смотрите документацию. Если uwsgi после запуска ругается на права, проверьте права на директорию с сокетом, на директорию с логами.
Создадим файл конфигурации /etc/uwsgi :
  1. PYTHONPATH=/srv/musicmans/root/src  
  2. MODULE=wsgi  

Обратите внимание, что мы указываем имя модуля python, а не имя файла.
Устанавливаем chmod 755 для скрипта /etc/init.d/uwsgi , загружаем при старте системы:
  1. root@musicmans:/var/lib/nginx# chown -R uwsgirun uwsgi  
  2. root@musicmans:/etc/init.d# chmod 755 uwsgi  
  3. root@musicmans:/etc/init.d# update-rc.d -f uwsgi defaults  
  4. root@musicmans:/etc/init.d# /etc/init.d/uwsgi start  

Конфиги nginx: nginx.conf, стандартный из пакета debian. Конфиг сайта:
  1. root@musicmans:/etc/nginx/sites-available# cat musicmans  
  2. #serving Django.  
  3. upstream django {  
  4.     ip_hash;  
  5.     server unix:/var/lib/nginx/uwsgi/musicmans.sock;  
  6.   }  
  7.   
  8. server {  
  9.      listen      80;  
  10.      server_name musicmans.ru;  
  11.      charset     utf-8;  
  12.      error_log   /srv/musicmans/logs/nginx_error.log   info;  
  13.      access_log /srv/musicmans/logs/nginx_access.log;  
  14.   
  15.      # Django admin media.  
  16.      #location /media/admin/ {  
  17.      #               alias lib/python2.6/site-packages/django/contrib/admin/media/;  
  18.      #               }  
  19.   
  20.      # Your project's static media.  
  21.      location /media/ {  
  22.                   alias /srv/musicmans/root/www/media/;  
  23.                          }  
  24.   
  25.      # Finally, send all non-media requests to the Django server.  
  26.      location / {  
  27.                  uwsgi_pass  django;  
  28.                  include     uwsgi_params;  
  29.                  }  
  30.   
  31.      location ~ /.svn/ {  
  32.            deny all;  
  33.                }  
  34.   
  35.        }  

Включаем сайт
  1. # ln -s /etc/nginx/sites-available/musicmans /etc/nginx/sites-enabled/musicmans  

Перезапускаем /etc/init.d/uwsgi restart и /etc/init.d/nginx restart.

Заходим http://musicmans.ru/:



Процесс развертывания кода и структуры базы данных на сервер с помощью fabric

Установим на машину разработчика pip и fabric.
  1. #pip install fabric  

Создаем fab файл с командами fabric в корне проекта для установки необходимых приложений из requirements.txt, обновления кода, миграции базы данных и перезапуска Nginx:

* Включить режим обслуживания сайта (см. выше).
* Сделать резервную копию базы данных.
* Сделать резервную копию кода (src) сайта.
* Обновить код с репозитория subversion.
* Запустить миграцию базы данных (South).
* Выключить режим обслуживания сайта.

У нас fabric 0.9.1, а в 1.0 обещают поддержку django. Ну а пока ее нет создаем fabfile.py в корне проекта следующего содержания (перевод windows консоли для понимания удаленного UTF8 в случае ошибок - шрифт cmd окна Lucida Console (или любой другой true type), далее команда chcp 65001).

  1. # -*- mode: python; coding: utf-8; -*-  
  2. import sys  
  3. from fabric.api import env, run, prompt, local, get, cd, sudo, require  
  4. from fabric.state import output  
  5. from fabric.contrib.files import uncomment  
  6. import datetime  
  7.   
  8. now = datetime.datetime.now()  
  9.   
  10. def production():  
  11.     #здесь данные об удаленном сервере с сайтом  
  12.     env.environment = "production"  
  13.       
  14.     env.hosts = ['codesrv']  
  15.     env.user = 'vermus'  
  16.     env.path = '/srv/musicmans/root'  
  17.     env.root_path = '/srv/musicmans'  
  18.       
  19.     env.db_name = 'musicmans'  
  20.     env.db_user = 'musicmans'  
  21.       
  22. def deploy():  
  23.     """ 
  24.     In the current version fabfile no initial database creation and configure the virtual server host. 
  25.     """   
  26.     require('environment', provided_by=[production])#дописать по желанию dev и stage  
  27.       
  28.     if env.environment == 'production':  
  29.         if "y" != prompt('Are you sure you want to update the production site (test & check in trunk release code!)? (y/[n])?', default="n"):  
  30.             return  
  31.       
  32.     if "y" == prompt('Set MAINTENANCE_MODE (y/n)?', default="y"):  
  33.         maintenance_mode()   
  34.       
  35.     if "y" == prompt('Create database backup? (y/n)?', default="y"):  
  36.         backup_db()  
  37.           
  38.     if "y" == prompt('Create source code backup? (y/n)?', default="y"):  
  39.         backup_src()  
  40.   
  41.     update_from_svn()  
  42.   
  43.     if "y" == prompt('Install the necessary applications (y/n)?', default="n"):  
  44.         install_requirements();  
  45.   
  46.     migrate_database()  
  47.   
  48.     maintenance_mode(set=False)      
  49.   
  50.     restart_webserver()          
  51.   
  52. def install_requirements():  
  53.     require('environment', provided_by=[production])#дописать по желанию dev и stage  
  54.     print(" * install the necessary applications...")  
  55.       
  56.     requirements_file = env.path+'/src/requirements.txt'  
  57.       
  58.     args = ['install',  
  59.                 '-r', requirements_file,  
  60.                 '--download-cache''/usr/src/pipcache/'  
  61.                 ]  
  62.       
  63.     sudo('pip %s' % ' '.join(args))  
  64.   
  65. def maintenance_mode(set=True):  
  66.     require('environment', provided_by=[production])#дописать по желанию dev и stage  
  67.     print(" * change production.py and restart nginx...")  
  68.     if set:  
  69.         uncomment(env.path+'/src/settings/production.py''MAINTENANCE_MODE = True')  
  70.     else:  
  71.         comment(env.path+'/src/settings/production.py''MAINTENANCE_MODE = True')  
  72.           
  73.     restart_webserver()   
  74.   
  75. def backup_db():  
  76.     require('environment', provided_by=[production])#дописать по желанию dev и stage  
  77.     print(" * create database dump...")  
  78.       
  79.     db_name = env.db_name  
  80.     db_user = env.db_user  
  81.   
  82.     backup_file = "backup_%d_%d_%d_%d_%d.sqlgzip" % (now.day, now.month, now.year, now.hour, now.minute)  
  83.     backup_dir = env.root_path+'/backups/db'  
  84.     with cd(backup_dir):  
  85.         run("echo dbpassword | pg_dump -W -U %s -F c %s > %s" % (db_user, db_name, backup_file))  
  86.   
  87. def backup_src():  
  88.     require('environment', provided_by=[production])#дописать по желанию dev и stage  
  89.     print(" * create source code backup...")  
  90.     backup_dir = env.root_path+'/backups/src'  
  91.     backup_file = "backup_%d_%d_%d_%d_%d.tar.gz" % (now.day, now.month, now.year, now.hour, now.minute)  
  92.     src_dir = env.path+'/src'  
  93.       
  94.     run("mkdir -p %s" % backup_dir+'/all')  
  95.     run("cp -f -R %s %s" % (src_dir, backup_dir+'/all'))  
  96.     run("cp -f -R %s %s" % (env.path+'/www/static', backup_dir+'/all'))  
  97.       
  98.     with cd(backup_dir):  
  99.         run ('tar -zcf %s %s' % (backup_file, backup_dir+'/all'))  
  100.         run ('rm -f -R %s' % (backup_dir+'/all'))  
  101.           
  102.          
  103. def update_from_svn():  
  104.     require('environment', provided_by=[production])#дописать по желанию dev и stage  
  105.     with cd(env.path):  
  106.         run('svn update'#svn checkout сделаем вручную первый раз  
  107.   
  108. def migrate_database():  
  109.     require('environment', provided_by=[production])#дописать по желанию dev и stage  
  110.     with cd(env.path+'/src'):  
  111.         run('python manage.py migrate --no-initial-data')  
  112.         run('python manage.py syncdb')  
  113.           
  114. def restart_webserver():  
  115.     require('environment', provided_by=[production])#дописать по желанию dev и stage  
  116.     print(" * restart nginx")  
  117.     sudo('/etc/init.d/uwsgi restart', pty=True)  
  118.     sudo('/etc/init.d/nginx force-reload', pty=True)  

Схема такая:
Запуск $fab production deploy с машины разработчика - логин по ssh на сервер с сайтом (fabric выполняет автоматически при выполнении run, sudo и др., используя данные из env), далее выполняются необходимые действия, в том числе svn+ssh с сервера с кодом с транка.

Добавим в /etc/postgresql/8.4/main/pg_hba.conf следующую строчку:
  1. # "local" is for Unix domain socket connections only  
  2. local   musicmans   musicmans                         md5  
  3. local   all         all                               ident  

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

Статья получилась объемной, старался быстрее закончить с технической стороной и перейти наконец к созиданию. :)

Не забываем про задачи (перспектива planning), указываем время, потраченное на задачи и закрываем их.

ps. Если вы хотите сразу обеспечить полноценную защиту сайта от различных атак, которая требует более тонкой настройки системы, то следует обратиться к профессионалам или почитать соответствующую литературу. Нам пока некому завидовать, поэтому оставляем все как есть на данном этапе.

10 комментариев:

  1. Сделайте, пожалуйста, кат. Очень неудобно такие портянки в ридере проматывать.

    ОтветитьУдалить
  2. 2Arts надо нажать J в ридере и не мотать, зато так можно спокойно читатать напрямую в ридере

    ОтветитьУдалить
  3. Ошибка в конфиге сайта - в секции, где вырубается svn, нужно передвинуть кавычку

    ОтветитьУдалить
  4. Непонятно, зачем в fabfile.py есть import sys, обращений я не увидел. Может заменить print на кошерный sys.stdout?
    И кстати, когда продолжение ? :-)

    ОтветитьУдалить
  5. Doctor013, да, лишний, остался от экспериментов. А насчет print, можно поподробнее, чем он плох, я что-то не в курсе насчет этого...
    Продолжение пишется, к середине недели наверное будет, спасибо за интерес :)

    ОтветитьУдалить
  6. C print ничего страшного :-), я употребил слово "кошерный". Подробнее - http://diveintopython.org/scripts_and_streams/stdin_stdout_stderr.html

    ОтветитьУдалить
  7. "When you print something, it goes to the stdout pipe;"
    "stdout is a file-like object; calling its write function will print out whatever string you give it. In fact, this is what the print function really does; it adds a carriage return to the end of the string you're printing, and calls sys.stdout.write. "
    Кошерный - идиологически верный, как я понимаю.
    Не понимаю, чем лучше sys.stderr.write(), а не print(), если только отсутствием возврата каретки? %)

    ОтветитьУдалить
  8. Vermus, проконсультируйте с правами. Вот эта строчка:
    root@musicmans:/var/lib/nginx# chown -R uwsgirun uwsgi делает владельцем uwsgirun, но при старте nginx владелец переопределяется на www-data. Это правильно?

    ОтветитьУдалить
  9. >Doctor013 комментирует...
    Да, действительно, похоже это лишняя строчка. Тем более лишнее -R, ибо сам сокет у нас 666.

    Предлагаю выполнить,
    1. Создать группу uwsgirun (в него пользователя uwsgirun).
    2. root@musicmans:/var/lib/nginx# chmod +t uwsgi
    это стики на всякий случай.
    3. root@musicmans:/var/lib/nginx# chown www-data:uwsgirun uwsgi
    4. root@musicmans:/var/lib/nginx# chmod 570 uwsgi
    грубо, чтение для владельца (www-data), запись для группы (uwsgirun)

    Ваши предложения?

    Кстати, я тут подумал, а надо ли рестартовать nginx вообще. Надо поэкспериментировать на эту тему.

    ОтветитьУдалить