Разворачиваем Django приложение в production на примере Telegram бота
Данная заметка это продолжение статьи про написание telegram бота, в ней я постараюсь максимально подробно осветить тему разворачивания (deploy) полноценного, хотя и маленького, Django приложения в production среде на ОС Linux, Ubuntu 14.04 LTS. К концу статьи у нас будет полноценный telegram бот, крутящийся в вебе и принимающий команды от пользователей этого мессенджера.
Чему вы научитесь после прочтения заметки:
- Разворачивать Django приложение (да и любое WSGI приложение) на хостинге Digital Ocean в среде Linux
- Работать с веб-серверами nginx и gunicorn
- Управлять процессами, используя утилиту supervisord
- Настраивать virtualenv с помощью pyenv
- Автоматически запускать веб-приложение даже после перезагрузки сервера
В сентябре 2015 года мы проводили Python митап в Алматы на котором я выступал с докладом на тему веб-разработки на Python. Во время выступления я вкратце описал веб эко-систему Python и сделал краткий обзор популярного инструментария. К сожалению, формат митапа не предусматривал детальный разбор темы, поэтому новичкам в этой области обычно приходится дальше копаться самостоятельно. Сегодня я постараюсь восполнить этот пробел и немного углубиться в "горячую" тему деплоя веб приложений на Python. Несмотря на то, что в статье речь будет идти о Django приложении, описываемые рецепты будут актуальны и для других веб-проектов, разработанных на Python с использованием WSGI-совместимых фреймворков (Flask, Bottle, Pyramid, Falcon, web2py и так далее).
В заметке я буду делать деплой на виртуальном хостинге от Digital Ocean. Если вы зарегистрируетесь по этой ссылке, то после подтверждения платёжных данных, счёт вашего аккаунта сразу пополнится на $10, которые можно потратить на создание маленьких дроплетов (виртуальных серверов) и потренироваться в разворачивании веб проектов на Python. Сразу скажу, что вам необязательно всё делать на удалённой машине и вообще использовать хостинг-провайдер, можно обойтись и локальной виртуалкой, например, используя VirtualBox и Vagrant (но в таком случае невозможно будет установить webhook).
Создание виртуального сервера
Как я ранее уже упоминал, деплой мы будет производить на одном из виртуальных серверов DigitalOcean с его мощным API :)
Создаём дроплет, нажимая на "Create droplet" в правом верхнем углу панели управления:
Выбираем самый минимальный тариф за 5 долларов в месяц с операционной системой Ubuntu 14.04.4 LTS на борту будущей виртуальной машины.
В качестве дата-центра я практически всегда выбираю Frankfurt, так как до него у меня самый лучший пинг. После заполнения всех необходимых полей, нажимаем кнопку "Create". Дроплет создаётся в течение 60 секунд после которых на почту поступает вся необходимая для доступа информация о новой виртуальной машине: IP адрес, логин и пароль.
Настройка сервера
После успешного создания нам необходимо авторизоваться на сервере. Сразу после входа, система попросит установить новый пароль для суперпользователя root.
Обновляем пакеты:
# apt-get update
# apt-get -y upgrade
Далее необходимо завести отдельного sudo пользователя из под которого будем запускать Django приложение.
# adduser django
# adduser django sudo
Заходим под новым юзером django на сервер, и все остальные команды выполняем из под данного юзера.
Устанавливаем необходимый арсенал для настройки виртуального окружения через Pyenv и сборки самой последней версии Python (2.7.11).
$ sudo apt-get install -y build-essential
$ sudo apt-get install -y python-dev libreadline-dev libbz2-dev libssl-dev libsqlite3-dev libxslt1-dev libxml2-dev
$ sudo apt-get install -y git
После этого ставим сам Pyenv. Подробнее о том что такое Pyenv и как его настроить можно прочитать здесь:
Устанавливаем Python самой последней версии (Python 2.7.11):
$ pyenv install 2.7.11
Downloading Python-2.7.11.tgz...
-> https://www.python.org/ftp/python/2.7.11/Python-2.7.11.tgz
Installing Python-2.7.11...
Installed Python-2.7.11 to /home/django/.pyenv/versions/2.7.11
Выполнение команды займёт некоторое время (скрипт скачает Python и скомпилирует его из исходников). Устанавливая отдельный интерпретатор питона мы тем самым никак не влияем на работу системного, более того, в последней LTS версии Ubuntu (14.04) используется версия 2.7.6, в которой существует ряд серьёзных уязвимостей, включая баг с SSL, а также отсутствует поддержка TLS 1.2
Клонируем репозиторий с Django проектом:
$ cd ~
$ git clone https://github.com/adilkhash/planetpython_telegrambot.git
$ cd planetpython_telegrambot/
Далее настраиваем виртуальное окружения Python, используя всё тот же pyenv.
$ pyenv virtualenv 2.7.11 telegram_bot
$ pyenv local telegram_bot
Ставим зависимости через менеджер пакетов pip.
pip install -r requirements.txt
Django приложение, написанное в первой части, претерпело незначительные изменения. В частности я перенёс изменяемые части кода в специальный .env файл, используя библиотеку django-environ. Ознакомиться с изменениями можно по этой ссылке.
Создаём .env файл из шаблона и заполняем необходимые настройки.
$ cd blog_telegram && mv .env-template .env && vi .env
В частности необходимо изменить режим DEBUG на False, прописать токен для Telegram бота и указать дополнительный хост через запятую в ALLOWED_HOSTS. В моём случае ALLOWED_HOSTS выглядит вот так:
ALLOWED_HOSTS=127.0.0.1,bot.khashtamov.com
То есть я завёл дополнительный поддомен на котором и будет крутиться Telegram бот.
Настройка SSL сертификата
В прошлой статье я писал о том, что в случае использования API вызова setWehook, хосту необходимо иметь валидный SSL сертификатом (Telegram позволяет использовать также самоподписанные сертификаты). Сертификат мы будет создавать через бесплатный сервис выдачи SSL сертификатов Let's Encrypt.
$ cd ~ && git clone https://github.com/letsencrypt/letsencrypt
$ cd letsencrypt/
$ ./letsencrypt-auto certonly --standalone -d bot.khashtamov.com
Необходимо будет указать некоторые настройки и согласиться с условиями предоставления услуг. После успешного выполнения команд, сертификаты будут находиться в /etc/letsencrypt/live/bot.khashtamov.com/
Настройка Nginx
Теперь пора поставить популярный HTTP сервер nginx который в нашем случае будет выполнять роль проксирующего (принимать запросы от клиентов и передать их дальше следуя инструкциям в конфигурационном файле).
$ sudo apt-get install -y nginx
$ cd /etc/nginx/sites-available/
$ sudo nano telegram_bot.conf
Заполняем новый файл telegram_bot.conf следующим содержимым:
server {
listen 80;
listen 443 ssl;
server_name bot.khashtamov.com;
ssl_certificate /etc/letsencrypt/live/bot.khashtamov.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/bot.khashtamov.com/privkey.pem;
location / {
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_pass http://localhost:8001/;
}
}
ВНИМАНИЕ: Не забудьте заменить хост bot.khashtamov.com на свой собственный.
Прописываем наш новый конфиг в настройки nginx и перезагружаем его, чтобы изменения вступили в силу:
$ cd /etc/nginx/sites-enabled/
$ sudo ln -s ../sites-available/telegram_bot.conf telegram_bot.conf
$ sudo service nginx restart
Что мы только что сделали?
- Прописали валидный SSL серификат для нашего сайта
- Все запросы, поступающие на хост, будут проксироваться на наше будущее Django приложение, которое в свою очередь, должно крутиться на 8001 порту.
- Передаём дополнительные HTTP заголовки в каждом запросе (хост, IP адрес клиента, схему https/http и так далее). Более подробно про настройки nginx можно прочитать здесь.
Чтобы проверить успешность наших настроек, можно запустить django приложение через тестовый сервер командой runserver на 8001 порту и зайти на сайт:
$ cd ~/planetpython_telegrambot/
$ python manage.py runserver 8001
Открываем браузер и видим (я сразу открыл через https):
URL Not Found это нормальное явление, так как у нас задан всего 1 валидный URL для непосредственной работы с Telegram - /planet/bot/<BOT_TOKEN>/ (не считая настройки Django админки).
Настройка Gunicorn через Supervisor
Пора приступить к настройке production-ready HTTP сервера Gunicorn, который, кстати, полностью написан на языке Python и хорошо зарекомендовал себя в реальном бою (к слову, во всех "живых" проектах я использую именно эту связку: nginx+gunicorn)
Что такое Supervisor?
Supervisor это утилита процесс-менеджер. Она "следит за здоровьем" ваших процессов-демонов и в случае их падения, старается снова их поднять. Если в ходе работы Gunicorn "падает" (системная ошибка, не та фаза луны и так далее), Supervisor старается его снова "поднять", таким образом работоспособность сайта не страдает. К слову, у меня в планах есть идея написать небольшую заметку про эту утилиту, так сказать Supervisor Advanced Usage. Стоит отметить, что все процессы, запущенные в Supervisor должны работать в foreground режиме, чтобы утилита понимала когда что-то идёт не по плану.
Для начала составим конфигурационный файл для запуска Gunicorn внутри Supervisor. Его содержимое выглядит вот так:
[program:gunicorn]
command=/home/django/.pyenv/versions/telegram_bot/bin/gunicorn blog_telegram.wsgi:application -b 127.0.0.1:8001 -w 1 --timeout=60 --graceful-timeout=60 --max-requests=1024
directory=/home/django/planetpython_telegrambot/
user=django
redirect_stderr=True
stdout_logfile=/tmp/gunicorn.log
stderr_logfile=/tmp/gunicorn_err.log
autostart=true
autorestart=true
startsecs=10
stopwaitsecs=10
priority=999
Сохраняем файл под именем gunicorn.conf (~/planetpython_telegrambot/gunicorn.conf). К слову, Gunicorn прописан в зависимостях нашего проекта (requirements.txt) и так как мы его уже установили в наше окружение, то узнать путь исполняемого файла можно выполнив команду внутри активированного виртуального окружения (активация происходит автоматически при переходе в директорию веб-приложения из-за наличия там файла .python-version, созданного через pyenv local):
$ pyenv which gunicorn
Содержимое конфигурационного файла для supervisord:
[unix_http_server]
file=/tmp/telgram_bot_supervisord.sock
[supervisord]
logfile=/tmp/telgram_bot_supervisord.log
logfile_maxbytes=50MB
logfile_backups=10
loglevel=info
pidfile=/tmp/telgram_bot_supervisord.pid
nodaemon=false
minfds=1024
minprocs=200
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///tmp/telgram_bot_supervisord.sock
[include]
files = /home/django/planetpython_telegrambot/gunicorn.conf
Сохраняем в ~/planetpython_telegrambot/supervisord.conf
Далее выполняем запуск Supervisor демона командой:
$ supervisord
Запуск должен пройти без каких либо ошибок. Чтобы узнать статус текущих процессов, запускаем утилиту supervisorctl:
$ supervisorctl
gunicorn RUNNING pid 20901, uptime 0:04:18
supervisor>
Для получения помощи, можно выполнить команду help. А для получения информации о команде - help . Например:
supervisor> help stop
stop Stop a process
stop :* Stop all processes in a group
stop Stop multiple processes or groups
stop all Stop all processes
supervisor>
После успешного запуска supervisor, сайт должен быть доступен онлайн.
Автозапуск веб-приложения при перезагрузке
А что будет, если наш виртуальный сервер внезапно перезагрузится? (сбой в дата-центре, неполадки на хост машине, криворукий админ накосячил и т.д.). В случае такого сценария, сейчас наше приложение не будет запущено автоматически. Чтобы это исправить необходимо приложить ещё немного усилий дабы написать небольшой скрипт, который мы успешно поместим в механизм автозагрузки ОС Ubuntu (Debian-like дистрибутивов).
Доводилось ли вам слышать про так называемые upstart файлы? Именно написанием одного из них мы сейчас и займёмся. К слову, на текущий момент Upstart признана устаревшей и в новых версиях ОС на базе Linux планируется полный переход на systemd.
description "Supervisor Telegram bot django app starting handler"
start on runlevel [2345]
stop on runlevel [!2345]
respawn
setuid django
setgid django
chdir /home/django/planetpython_telegrambot/
exec /home/django/.pyenv/versions/telegram_bot/bin/supervisord
Файл должен быть помещён в /etc/init/ (в моём случае я дал ему имя telegram_bot.conf). Если ранее все запуски не вызывали проблем, то после рестарта системы, приложение автоматически будет запущено:
$ sudo shutdown -r now
Теперь необходимо прописать наш URL на стороне Telegram используя вызов API метода setWebhook:
import telepot
bot_token = 'BOT_TOKEN'
bot = telepot.Bot(bot_token)
bot.setWebhook('https://bot.khashtamov.com/planet/bot/{bot_token}/'.format(bot_token=bot_token))
На этом настройка бота закончена. Посылаем команды нашему боту @PythonPlanetBot и получаем адекватные ответы :)
В последней версии Django приложения я добавил:
- логирование запросов от пользователей в файл
- вынес изменяемые настройки (режим debug, токен бота, секретный ключ) в переменные окружения через .env файлы, используя django-environ
- добавил шаблоны конфигурационных файлов для gunicorn, nginx и supervisor