Разворачиваем 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 адрес, логин и пароль.

Новый дроплет за 5 баксов

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

После успешного создания нам необходимо авторизоваться на сервере. Сразу после входа, система попросит установить новый пароль для суперпользователя root.

Суперпользователь 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):

Django Nginx SSL

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 и получаем адекватные ответы :)

Наш чудо-telegram бот

В последней версии Django приложения я добавил:

  • логирование запросов от пользователей в файл
  • вынес изменяемые настройки (режим debug, токен бота, секретный ключ) в переменные окружения через .env файлы, используя django-environ
  • добавил шаблоны конфигурационных файлов для gunicorn, nginx и supervisor

Полезные ссылки: