Запуск web приложения на Flask в Colab

При быстром написании Python web приложений на Flask лень что-то устанавливать на локальном ПК. Хочется ,быстро запустить в облаке Google Colab без лишних танцев с бубнами и получить доступ к серверу по некоторому доменному имени.

Для проброса web обращений к приложению за NAT-ом есть немало сервисов. Наиболее популярный — ngrok. Альтернативный вариант localtunnel.

Ngrok для доступа к веб приложению снаружи

Самый простой вариант пробросить запущенный на локальных IP адресах сервер — использовать популярный сервер ngrok.

Вариант 1.

!pip install flask-ngrok
from flask import Flask
from flask_ngrok import run_with_ngrok

app = Flask(__name__)
run_with_ngrok(app)   
  
@app.route("/test")
def home():
    return "<h1>GFG is great platform to learn</h1>"
    
app.run()

После выполнеия кода получим строчку вроде:

 * Serving Flask app "__main__" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Running on http://1680e48e0f6a.ngrok.io
 * Traffic stats available on http://127.0.0.1:4040

В строке «Running on http://1680e48e0f6a.ngrok.io» — указан URL доступный снаружи.

Вариант 2.

Библиотека flask_ngrok довольно старая и она не поддерживает командую строку для запуска ngrok с параметрами, например, строкой авторизации или поддержкой subdomain.

Subdomain — очень важный параметр, позволяющий задать не рандомный домен третьего уровня в 1680e48e0f6a.ngrok.io, а заданный, например, test20210701.ngrok.io. Естественно, домен должен быть свободен.

Для запуска воспользуемся регулярно обновляемой и хорошо документируемой библиотекой pyngrok.

import os
import threading

!pip install pyngrok

from flask import Flask
from pyngrok import ngrok

os.environ["FLASK_ENV"] = "development"

app = Flask(__name__)
port = 5000

#Setting an auth token allows us to open multiple tunnels at the same time
ngrok.set_auth_token("<YOUR_AUTH_TOKEN>")

# Open a ngrok tunnel to the HTTP server
public_url = ngrok.connect(port).public_url
print(" * ngrok tunnel \"{}\" -> \"http://127.0.0.1:{}\"".format(public_url, port))

# Update any base URLs to use the public ngrok URL
app.config["BASE_URL"] = public_url

# Define Flask routes
@app.route("/")
def index():
    return "Hello from Colab!"

# Start the Flask server in a new thread
threading.Thread(target=app.run, kwargs={"use_reloader": False}).start()

После запуска кода получаем сообщение вида:

 * ngrok tunnel "http://ae7250c89181.ngrok.io" -> "http://127.0.0.1:5000"
 * Serving Flask app "__main__" (lazy loading)
 * Environment: development
 * Debug mode: on

Если в строке соединения указать поддомен третьего уровня, который мы хотим получить

public_url = ngrok.connect(subdomain="foo20210701")

то получим сообщение об ошибке вида «Only paid plans may bind custom subdomains.\nFailed to bind the custom subdomain ‘foo20210701’ for the account ‘XXXXXX’.\nThis account is on the ‘Free’ plan.\n\nUpgrade to a paid plan at:»

Описание тарифных планов для ngork указано здесь. Минимальный тарифный план на котором можно использовать поддомены стоит 5 USD в мес.

Запуск Gunicorn на Colab

Gunicorn ‘Green Unicorn’ — это Python WSGI HTTP север для UNIX. Запустим этот сервер на Colab. Для начала создам простой тестовый Python web скрипт.

def generateWebApp():
  text =  "from flask import Flask\r\n"\
          "app = Flask(__name__)\r\n"\
          "\r\n"\
          "@app.route(\"/\")\r\n"\
          "def hello():\r\n"\
          " return \"Hello Localtunnel\"\r\n"\
          "\r\n"\
          "if __name__ == \"__main__\":\r\n"\
          " app.run()\r\n"

  #print(text)
  with open('webapp.py', 'w') as f:
    f.write(text)  

generateWebApp()

Запустим

!pip install gunicorn
!gunicorn --workers 2 -b localhost:8000 webapp:app

После запуска gunicorn в Colab получим такой результат:

[2021-07-04 03:16:34 +0000] [332] [INFO] Starting gunicorn 20.1.0
[2021-07-04 03:16:34 +0000] [332] [INFO] Listening at: http://127.0.0.1:8000 (332)
[2021-07-04 03:16:34 +0000] [332] [INFO] Using worker: sync
[2021-07-04 03:16:34 +0000] [335] [INFO] Booting worker with pid: 335
[2021-07-04 03:16:34 +0000] [336] [INFO] Booting worker with pid: 336
[2021-07-04 03:23:47 +0000] [332] [INFO] Handling signal: int
[2021-07-04 03:23:47 +0000] [335] [INFO] Worker exiting (pid: 335)
[2021-07-04 03:23:47 +0000] [336] [INFO] Worker exiting (pid: 336)
[2021-07-04 03:23:47 +0000] [332] [INFO] Shutting down: Master

Все запускается, но встает вопрос как обратится к веб скрипту запущенному в Colab под gunicorn снаружи, чтобы можно было обратится к нему по некоторому фиксированому поддомену.

Запуск localtunnel на Colab

Здесь на помощь придет localtunnel. В нем, в отличие от ngrok можно создавать subdomain бесплатно, что полезно для решения коротких тестовых задач.

localtunnel создает обратный SSH-туннель к публичному сервису обратного туннелирования, чтобы пробросить наружу локальный сервер. В данном случае Gunicorn поднят на 8000 порту на виртуальной машине Colab и нам надо пробросить к нему снаружи все обращения с домена с фиксированным именем https://smarthome161075.loca.lt.

Важный момент, в аргументе localtunnel в имени subdomain можно использовать ТОЛЬКО строчные символы. Иначе localtunnel не стартует. Ошибку не выдает.

#Deploy localtunnel on Colab
!npm install -g localtunnel
!lt --port 8000 --subdomain smarthome161075

В результатах получим сообщение:

your url is: https://smarthome161075.loca.lt

Все отрабатывает, но вот незадача, оба процесса Gunicorn и localtunnel стартуют в Colab как однозадачные, а нам надо, чтобы они работали параллельно.

Запуск нескольких процессов в Colab

Нам нужно, чтобы обе команды запуска Gunicorn и localtunnel отрабатывали независимо друг от друга. Для этого воспользуемся UNIX командой nohup. Например, чтобы запустить localtunnel как фоновый процесс нужно выполнить в Colab следующую команду:

!nohup lt —port 8000 —subdomain $domainName > lt.log 2>&1 &

При запуске localtunnel таким образом Colab запустит его в виде фонового процесса. В lt.log будут переадресованы все выводы localtunnel. Поскольку при запуске процесса таким образом мы не можем остановить его штатными средствами Colab, для того, чтобы его удалить придется воспользоваться командой kill. Но для её выполнения нужно получить PID запущенного процесса. Это делается следующим образом:

!ps -ef | grep lt

В результатах вывода получим что-то вроде:

root          52       1  0 03:46 ?        00:00:08 /usr/local/bin/dap_multiplexer --domain_socket_path=/tmp/debugger_bi5ptuks4
root         793      62  0 04:37 ?        00:00:00 /bin/bash -c lt --port 8000 --subdomain smarthome161075 > lt.log 2>&1 
root         794     793  0 04:37 ?        00:00:00 node /tools/node/bin/lt --port 8000 --subdomain smarthome161075
root        1107      62  0 05:29 ?        00:00:00 /bin/bash -c ps -ef | grep "lt"
root        1109    1107  0 05:29 ?        00:00:00 grep lt

Искомый процесс запущен с PID = 794. Для его принудительного завершения используем команду:

!kill 794

Аналогичным образом можем запустить в Colab Gunicorn как фоновый процесс:

!nohup gunicorn —workers 2 -b localhost:8000 webapp:app > gunicorn.log 2>&1 &
!ps -ef | grep gunicorn

В логе gunicorn.log получим всю информацию о PID-ах запущенных worker-ов. Или посмотреть процессы можно в результатах выдачи -ps:

root         360       1  0 04:06 ?        00:00:00 [gunicorn] <defunct>
root         515       1  0 04:24 ?        00:00:00 [gunicorn] <defunct>
root         599       1  0 04:28 ?        00:00:00 [gunicorn] <defunct>
root         779       1  8 04:37 ?        00:00:00 /usr/bin/python3 /usr/local/bin/gunicorn --workers 2 -b localhost:8000 webapp:app
root         785     779  3 04:37 ?        00:00:00 /usr/bin/python3 /usr/local/bin/gunicorn --workers 2 -b localhost:8000 webapp:app
root         786     779  3 04:37 ?        00:00:00 /usr/bin/python3 /usr/local/bin/gunicorn --workers 2 -b localhost:8000 webapp:app
root         787      62  0 04:37 ?        00:00:00 /bin/bash -c ps -ef | grep "gunicorn"
root         789     787  0 04:37 ?        00:00:00 grep gunicorn

Видим, что основной процесс с PID = 779 породил два дочерних процесса (workers), как заказывали, с PID: 785, 786. Срубив основной процесс с PID = 779 дочерние процессы также останавливаются.

Не очень удобно так останавливать запущенные процессы, поэтому воспользуемся штатным функционалом Colab для запуска процесса Gunicorn и localtunnel.

import threading

def execLocalTunnel(domainName):
  print("Exec localtunnel with domain name:", domainName)
  !lt --port 8000 --subdomain $domainName > lt.log 2>&1 
  
 
if __name__ == '__main__':
  my_thread = threading.Thread(target=execLocalTunnel, args=("smarthome161075",))
  my_thread.start()

При таком запуске localtunnel из-под Colab есть возможность остановить запущенный процесс нажав на крестик воле него.

По аналогии запустим Gunicorn под Colab:

def execGunicorn(port = 8000):
  print("Exec localtunnel with port:", port)
  !gunicorn --workers 2 -b localhost:$port webapp:app  > gunicorn.log 2>&1 &

if __name__ == '__main__':
  gunicorn_thread = threading.Thread(target=execGunicorn, args=(8000,))
  gunicorn_thread.start() 

Теперь можно запускать веб сервер на Colab и давать к нему доступ снаружи используя фиксированное имя домена, что удобно при отладке сервисов, чтобы не «городить огород» с контейнерами docker и пр.

Список литературы

Spread the love
Запись опубликована в рубрике IT рецепты с метками , . Добавьте в закладки постоянную ссылку.

Обсуждение закрыто.