Деплоим на PythonAnywhere из GitHub +7




Каждый может сделать так:


локальный проект > github


С (платным) ssh доступом вы сможете сделать так:


локальный проект > PythonAnywhere


В статье показано как (бесплатно) сделать так:


локальный проект > github > PythonAnywhere


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




Зачем?


PythonAnywhere — отличный сервис! Он бесплатен, предоставляет неплохие мощности и даже базу данных, таким образом вы можете поднять динамический сайт за пару минут. Это отличный вариант как для новичков, которые хотят попробовать что-нибудь потестировать вживую, так и для тех, кому нужно хостить API или какой-нибудь личный проект.


Но у сервиса есть и недостатки. Что если вы хотите заопенсорсить код, над которым работаете? Будете поддерживать и вносить изменения в двух местах сразу? Один раз на продакшене PythonAnywhere и второй раз на GitHub для других разработчиков. Что если вы примете Pull Request или захотите интегрировать CI? Постоянно дублировать все действия очень неудобно.


GitHub отличный сервис для совместной работы и просмотра исходного кода, их UI лучше чем на PythonAnywhere и, к чему я это веду, редактировать код прямо на PythonAnywhere не очень приятно. Что если можно было бы соединить лучшее от двух миров?


Вы пушите на GitHub все апдейты, а приложение на PythonAnywhere синхронизируется и перезапускается автоматически. Вы можете мерджить PR'ы, использовать тикеты, просматривать ваш код с любого устройства, даже не логинясь и не открывая файл в редакторе кода — в общем, делать всё что позволяет GitHub.


Я уже слышу ропот: «Хорошо-хорошо, убедил, но как же этого добиться?». Ни слова больше!




Как?


Мы используем вебхуки Гитхаба, чтобы оповестить об обновлении приложения, сделать pull и перезагрузить его.


В качестве примера я буду рассматривать моё приложение SwagLyrics, бэкенд которого держу на PythonAnywhere. Я использую Flask, так что процесс будет отличаться для другого фреймворка.


Для начала синхронизируем проект, чтобы GitHub был origin. Если вы ещё не создали репозиторий на PythonAnywhere, вы можете инициализировать его или склонировать код напрямую с GitHub.


Что-то типа:


git init 

git remote add origin https://github.com/yourusername/yourreponame.git

Теперь идём на GitHub > Settings > Webhooks > Add webhook


Там вы увидите:



В поле «Payload URL» добавьте your_domain/route_to_update


Например, вебхук моего репозитория указывает на nbsp;https://aadibajpai.pythonanywhere.com/update_server


Измените «Content type» с application/x-www-form-urlencoded на application/json (я расскажу вам зачем это нужно немного позже).


Поле «Secret» мы пока трогать не будем.


Убедитесь, что выбрана опция «push event» и кликните «Add webhook».


Открывайте своё приложение на Flask, мы сконфигурируем route для получения информации от GitHub при наступлении push события. Путь должен быть тем, который вы указали в поле «Payload URL». Мы не указываем ветку master явно, т.к. для простоты подразумевается, что она в репозитории единственная.


Простейшая настройка будет выглядеть так:


from flask import Flask, request
import git 

app = Flask(__name__) 

@app.route('/update_server', methods=['POST'])
    def webhook():
        if request.method == 'POST':
            repo = git.Repo('path/to/git_repo')
            origin = repo.remotes.origin

            origin.pull()

            return 'Updated PythonAnywhere successfully', 200
        else:
            return 'Wrong event type', 400 

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


Теперь всякий раз когда произойдёт событие push, приложение обновит себя, выполнив pull.


Если всё прошло гладко, вот что вы увидите после очередного коммита:



Прежде чем мы перейдём к защите вебхука от посторонних, я расскажу как перезапускать приложение после pull, чтобы этого не нужно было делать вручную.


Автоматическая перезагрузка веб-приложения


Мы будем использовать хуки git. Это просто команды оболочки, выполняемые после событий. Хука для события после pull нет, но...


Мы используем тот факт, что git pull — это не что иное, как git fetch > git merge, а вот хук для события после merge существует. Он выполняется, если вытягивание завершено успешно.


В вашем репозитории на PythonAnywhere перейдите в .git/hooks/


Там уже будет несколько существующих хуков, добавьте к ним свой, создав файл post-merge


Запишите в него такой код:


#!/bin/sh
touch /path/to/username_pythonanywhere_com_wsgi.py

Используйте путь к своему wsgi, который будучи изменённым (touch), перезапускает приложение.


Чтобы сделать файл исполняемым, откройте консоль и выполните


chmod +x post-merge

Убедитесь, что перезагрузка работает, сделав новый коммит.


Теперь перейдём к обеспечению защиты вебхука.




Защита вебхука


Защитить вебхук необходимо, чтобы кто-то другой не смог постоянно посылать запросы на перезагрузку приложения. Мы будем использовать этот гайд.


Во-первых, добавьте секретный токен на PythonAnywhere в качестве переменной среды, а также в поле «Secret» в настройках вебхука на GitHub. Здесь процесс расписан чуть подробнее.


GitHub отдаёт свои методы на Ruby, но мы используем Python, поэтому будем использовать такую функцию сравнения:


import hmac
import hashlib

def is_valid_signature(x_hub_signature, data, private_key):
    # x_hub_signature and data are from the webhook payload
    # private key is your webhook secret
    hash_algorithm, github_signature = x_hub_signature.split('=', 1)
    algorithm = hashlib.__dict__.get(hash_algorithm)
    encoded_key = bytes(private_key, 'latin-1')
    mac = hmac.new(encoded_key, msg=data, digestmod=algorithm)
    return hmac.compare_digest(mac.hexdigest(), github_signature)

Теперь измените контроллер update_server, чтобы проверять действительна ли подпись, добавив эти строки перед частью с обновлением кода:


x_hub_signature = request.headers.get(‘X-Hub-Signature’) 

if not is_valid_signature(x_hub_signature, request.data, w_secret): 

w_secret должен совпадать со значением, которое вы чуть раньше установили в качестве переменной среды.


Вообще, имеет смысл добавить ведение логов и ещё несколько проверок, чтобы убедиться, что вебхук от GitHub или что событие содержит данные, поэтому, если хотите, можете скопировать релевантный код из моего репозитория, изменив его там где нужно, благо вы уже знаете всё самое важное


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




Я не придумал всё выше сам, скорее собрал самое важное из разных источников и создал законченное решение.


Вот ссылки, которые помогли мне и могут помочь вам:
  1. https://stackoverflow.com/a/54268132/9044659 (базовая настройка)
  2. https://developer.github.com/webhooks/ (документация по вебхукам GitHub)
  3. https://github.com/CCExtractor/sample-platform/blob/master/mod_deploy/controllers.py (очень подробная реализация с кучей проверок, если хотите тратить на это время)
  4. https://github.com/SwagLyrics/swaglyrics-issue-maker/blob/35d23d0ba416e742e381da931d592ce6f58fc13f/issue_maker.py#L268 (моя реализация контроллера для PythonAnywhere)
  5. https://github.com/SwagLyrics/SwagLyrics-For-Spotify (репозиторий, где я использую это на практике)

Спасибо, что дочитали!




К сожалению, не доступен сервер mySQL