При быстром написании 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 и пр.
Список литературы
- Все приведенные в статье примеры работы с web сервером Flask на Google Colab.
- 4 лучшие альтернативы для ngrok.
- How to Deploy Web Application from Home.
- https://docs.gunicorn.org/en/stable/
- https://towardsdatascience.com/quickly-share-ml-webapps-from-google-colab-using-ngrok-for-free-ae899ca2661a
- https://pyngrok.readthedocs.io/en/latest/integrations.html
- https://ngrok.com/docs
- https://www.geeksforgeeks.org/how-to-run-flask-app-on-google-colab/
- https://towardsdatascience.com/how-to-deploy-ml-models-using-flask-gunicorn-nginx-docker-9b32055b3d0