Планирование и вызов блокнотов как веб-сервисов с помощью Jupyter API

Благодаря бессерверным облачным сервисам, таким как GCP CloudRunner и Cloud Functions, нам не нужно управлять дорогостоящими виртуальными машинами или серверами для развертывания блокнотов и их периодического запуска. С помощью Jupyter API можно поднимать блокноты в облако, превращать их в веб-сервисы и интегрировать с вашими сервисами.
Однако наиболее распространенным подходом (если вы не используете облачные нативные сервисы, такие как Vertex AI или SageMaker) является преобразование блокнотов в код на Python с помощью nbconvert и добавление этого кода в пользовательское веб-приложение Tornado или Flask.

Для этого необходимо использование внешних библиотек, но хорошей новостью является то, что мы можем оставить наш код в нашем контейнере разработки Jupyter и запускать его непосредственно оттуда, используя Jupyter Rest API.
Доступ к блокноту через Web API
Прежде чем мы перейдем к подробному описанию использования Jupyter API, я продемонстрирую, как будет работать архитектура нашего сервиса. Для начала возьмем простой блокнот, который мы можем использовать для тестирования.

Чтобы запустить его локально с помощью Jupyter, проще всего запустить его в контейнере Jupyter Lab:
# download the test workbook
wget https://raw.githubusercontent.com/tfoldi/vizallas/main/notebooks/JupyterAPI_Test.ipynb
# Spawn a new Jupyter lab instance with token auth (and without XSRF)
docker run -it --rm -p 8888:8888 \
-e JUPYTER_TOKEN=ab30dd71a2ac8f9abe7160d4d5520d9a19dbdb48abcdabcd \
--name testnb -v "${PWD}":/home/jovyan/work jupyter/base-notebook \
jupyter lab --ServerApp.disable_check_xsrf=true
После запуска сервиса вы сможете получить доступ к ноутбуку по адресу http://127.0.0.1:8888/lab/tree/work, используя токен, переданный в переменной окружения JUPYTER_TOKEN.
Вызов блокнота из командной строки
Из командной строки можно загрузить этот небольшой скрипт (для него требуются пакеты requests и websocket-client) или запустить его через контейнер Docker:
# check the IP address of our previously started "testnb" container
docker inspect testnb | grep IPAddress
"SecondaryIPAddresses": null,
"IPAddress": "172.17.0.2",
"IPAddress": "172.17.0.2",
# Invoke our notebook. Replace the IP below with yours from previous step.
docker run -it --rm \
-e JUPYTER_TOKEN=ab30dd71a2ac8f9abe7160d4d5520d9a19dbdb48abcdabcd \
tfoldi/jupyterapi_nbrunner 172.17.0.2:8888 /work/JupyterAPI_Test.ipynb
Creating a new kernel at http://172.17.0.2:8888/api/kernels
Sending execution requests for each cell
{'data': {'text/plain': '15'}, 'execution_count': 3, 'metadata': {}}
Processing finished. Closing websocket connection
Deleting kernel
Этот скрипт подключается к только что созданному серверу JupyterLab, выполняет наш блокнот, возвращает результат последней ячейки и завершает работу. Вся процедура происходит по веб-протоколу, не требуя никаких изменений в коде блокнота или дополнительных библиотек.
Что под капотом
Сначала необходимо инициализировать новое ядро (или использовать существующее), получить метаданные блокнота, получить все ячейки кода и отправить для каждой из них запрос execute_request.
Для получения результатов необходимо прослушать входящие сообщения в канале WebSocket. Поскольку сообщения “конец выполнения кода” не существует, приходится вручную отслеживать, сколько блоков кода мы отправили на выполнение и сколько из них действительно было выполнено, подсчитывая все сообщения типа execute_reply. После того как все выполнено, мы можем остановить ядро или оставить его в состоянии ожидания для последующих выполнений.
На следующей диаграмме показан весь процесс:

Чтобы оставаться аутентифицированными, мы должны передавать заголовок Authorization для всех вызовов HTTP и WebSocket.
Если вам кажется, что это слишком большое количество шагов для выполнения блокнота, то я вас понимаю. Я уверен, что было бы полезно реализовать функцию более высокого уровня внутри Jupyter Server, чтобы уменьшить сложность.
Полная версия скрипта находится здесь и может быть использована в ваших приложениях.
Расписание нашей рабочей тетради по GCP бесплатно (почти)
Хотя существует множество вариантов размещения ноутбука, наиболее экономически эффективным является использование сервиса Cloud Run компании Google Cloud. При использовании Cloud Run вы платите только за точное время выполнения задания, что делает его экономически эффективным выбором для нечасто запускаемых задач без дополнительных пакетов или дополнительных поставщиков SaaS (кроме Google) – и, опять же, без написания одной строки кода.
Архитектура и поток вызовов будут выглядеть следующим образом:

Сначала нам необходимо развернуть наш блокнот в GCP Cloud Run. Существует несколько способов добавить файл в службу Cloud Run, но, пожалуй, самый простой – скопировать наш блокнот в контейнер Docker.
# Simple dockerfile to host notebooks on a Jupyter Server
FROM jupyter/base-notebook
COPY JupyterAPI_Test.ipynb /home/jovyan/workspaces/
Чтобы собрать и сделать контейнер доступным в Cloud Run, мы можем просто указать опцию –source в команде gcloud run deploy, указав ей на каталог, в котором находятся наши блокноты и Dockerfile.
# get the source code of the Jupyter notebook and the Dockerfile
git clone https://github.com/tfoldi/jupyterapi_nbrunner.git
# Deploy the test notebook in a jupyter/base-notebook container
# The Dockerfile and JupyterAPI_Test.ipynb files in the tests/test_notebook
# folder
gcloud run deploy test-notebook --region europe-west3 --platform managed \
--allow-unauthenticated --port=8888 \
--source tests/test_notebook \
--set-env-vars=JUPYTER_TOKEN=ab30dd71a2ac8f9abe7160d4d5520d9a19dbdb48abcdabcd
[...]
Service [test-notebook] revision [test-notebook-00001-mef] has been deployed and is serving 100 percent of traffic.
Service URL: https://test-notebook-fcaopesrva-ey.a.run.app
JupyterLab будет доступен по URL-адресу сервиса. Google Cloud Run предоставит SSL-сертификаты и механизмы запуска или приостановки контейнера в зависимости от запросов, поступающих на развертывание.
Чтобы запустить наш развернутый блокнот из облачного планировщика, необходимо создать облачную функцию, привязанную к теме PubSub. Следующая команда развернет main.py и requirements.txt из этого репозитория. Main.py – это тот же скрипт, который мы использовали ранее для запуска нашего кода из командной строки.
# make sure you are in the same directory where you cloned the
# contents of https://github.com/tfoldi/jupyterapi_nbrunner.git
gcloud functions deploy nbtrigger --entry-point main --runtime python311 \
--trigger-resource t_nbtrigger --trigger-event google.pubsub.topic.publish \
--timeout 540s --region europe-west3 \
--set-env-vars=JUPYTER_TOKEN=ab30dd71a2ac8f9abe7160d4d5520d9a19dbdb48abcdabcd
Давайте протестируем нашу новую облачную функцию, отправив сообщение в тему t_nbtrigger с соответствующими параметрами, как мы это делали в командной строке:
gcloud pubsub topics publish t_nbtrigger \
--message="test-notebook-fcaopesrva-ey.a.run.app:443
/workspaces/JupyterAPI_Test.ipynb --use-https"
Если проверить журналы облачной функции nbtrigger, то можно заметить, что выдача записи в тему успешно спровоцировала выполнение указанного нами блокнота:

Последний шаг – создание расписания, которое запускается в указанное время. В данном случае мы собираемся запускать наш блокнот каждый час:
gcloud scheduler jobs create pubsub j_hourly_nbtrigger \
--schedule "0 * * * *" --topic t_nbtrigger --location europe-west3 \
--message-body "test-notebook-fcaopesrva-ey.a.run.app:443 /workspaces/JupyterAPI_Test.ipynb --use-https --verbose"
Все готово – вы только что запланировали свой первый Jupyter Notebook бессерверным способом.

Наш Notebook будет потреблять всего несколько центов в день, что делает этот способ развертывания одним из самых экономичных в Google Cloud.
