Разворачиваем 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. Ознакомиться с изменениями можно по этой ссылке.

Клонируем репозиторий с проектом:

$ git clone https://github.com/adilkhash/planetpython_telegrambot.git && cd planetpython_telegrambot

Создаём .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

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

  • Groosha

    Поясните, пожалуйста, выбор устаревшей версии Python (2.7, которая не получит новых фич в будущем).

    • Так и знал, что будет этот вопрос 🙂 Почему же устаревшей? Python 2.7 ещё до 2020 точно будет жить, а возможно и дальше. Но возможно действительно было бы лучше всё разворачивать на Python 3. Я думаю весь код полностью совместим, необходимо лишь вместо установки 2.7.11 ставить, например, 3.5.1. Правда в этом случае supervisor, установленный в окружение через pip, не будет работать. Его необходимо ставить через apt-get либо в системный Питон либо брать репозитория (вроде как там версия уже умеет работать с Python 3, правда ещё «сырая»).

      • Groosha

        Устаревший в том плане, что в Py2 не будет никаких новых фич из Py3, да и насчет оптимизаций/security fixes всё не так гладко.

        Если причина лишь в supervisor, то вопрос снимается, спасибо за оперативный ответ.

        • Нет, вовсе не в supervisor. Скорее по привычке. Надеюсь с выходом ubuntu 16.04 LTS такая привычка исчезнет сама собой 🙂

  • А почему не использовать docker? Сразу можно было бы забыть о pyenv, virtualenv, деплое вручную, etc

    • Согласен, но не стал усложнять статью (она ведь для новичков). Docker это тема отдельной заметки, думаю напишу совсем скоро.

      • Да-да, про докер надо. При чем мне не попадались еще статьи в духе «для совсем лохов» 🙂
        Все хочу попробовать переехать на докер с целью упрощения, но из всех статей, прочитанных по-диагонали мной делаю вывод, что слишком много гемора.

        • Занёс написание статьи в свой план 🙂

  • Pingback: Python-RQ: очередь задач на базе Redis — Персональный блог Адиля Хаштамова()

  • Pingback: Как написать Telegram бота: практическое руководство — Персональный блог Адиля Хаштамова()

  • Сергей

    Подскажите, устанавливаю на raspberry pi ОС raspbian. На шаге настройки виртуального окружения Python ошибка:
    ~/planetpython_telegrambot $ pyenv local telegram_bot
    pyenv: version `telegram_bot’ not installed

    pyenv versions выдаёт следующее:
    ~/planetpython_telegrambot $ pyenv versions
    * system (set by /home/django/.pyenv/version)
    2.7.12

  • Евгений

    Приветствую! Как вы считаете, можно ли организовать на телеграм следующую вещь: создать простенького приватного бота для телеграм, принцип работы следующий:
    1. Бот полностью приватен, тоесть ели боту пишут не с номера владельца или же не с тех номеров, которые владелец разрешил на взаимодействие с ним, бот тупо отвечает сообщение о том, что доступ запрещен и для получения доступа… бла бла бла
    2. Бот выдает меню, например видео архив1, видео архив2, видео архив3 и т.д. Если пользователь к примеру выбирает один из пунктов, то бот выдает список доступных к просмотру видеофайлов, причем именно список в виде пунктов меню или ссылок
    3. Тут начинается самое интересное, нажимаем на определенную ссылку(пункт) и бот открывает соответствующий видеофайл, который находится в лучшем конечно случае чате (вот к примеру у меня есть супергруппа и в ней есть в загрузке медиафайлы, телеграм фасует их по папкам вот так https://s.mail.ru/HkpQ/P19t1uQN5) было бы круто, чтобы бот тянул файл с определенным названием именно оттуда. Это было бы удобно, потому как пользователи в группе, они имеют прямое отношение к данному боту, и впринципе имеют доступ к этим файлам и без него, просто смысл бота заключается в том, что бы помочь пользователю быстро найти нужный видеофайл.. Короче навигация одним словом

    • Доброго времени суток! Я думаю, что вполне возможно такую штуку реализовать)

  • Максим Лагер

    За фичи с Supervisor и upstart спасибо, но прямо очень нужен asyncio) Можете коснутся этой темы?