Programar e invocar cuadernos como servicios web utilizando la API de Jupyter

Programar e invocar cuadernos como servicios web con API de Jupyter

Gracias a los servicios en la nube sin servidor como GCP CloudRunner y Cloud Functions, no necesitamos administrar costosas máquinas virtuales o servidores para implementar nuestros cuadernos y ejecutarlos periódicamente. Con la API de Jupyter, puedes trasladar tus cuadernos a la nube, convertirlos en servicios web e integrarlos con programación.

Cuaderno de Python programado en la nube, generado por MidJourney, instruido por el autor

Sin embargo, el enfoque más comúnmente utilizado (a menos que estés utilizando servicios nativos de la nube como Vertex AI o SageMaker) es convertir los cuadernos a código Python utilizando nbconvert y agregar el código a una aplicación web personalizada recién configurada de Tornado o Flask.

Contenerización tradicional de un cuaderno de Python sin, imagen por el autor

Esto incluye algo de programación y bibliotecas externas, pero la buena noticia es que podemos dejar nuestro código en nuestro contenedor de desarrollo de Jupyter y activarlo directamente desde allí utilizando la API de Rest de Jupyter.

Acceso a un cuaderno a través de la API web

Antes de entrar en los detalles de cómo utilizar la API de Jupyter, demostraré cómo funcionará la arquitectura. Primero, tomemos un cuaderno simple que podamos usar para realizar pruebas.

Cuaderno de prueba simple que devuelve "15" si todo funciona bien.

Para ejecutarlo localmente usando Jupyter, la forma más sencilla es ejecutarlo en un contenedor de Jupyter Lab:

# descargar el cuaderno de pruebawget https://raw.githubusercontent.com/tfoldi/vizallas/main/notebooks/JupyterAPI_Test.ipynb# Iniciar una nueva instancia de Jupyter lab con autenticación de token (y sin 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

Una vez que el servicio se inicie, podrás acceder al cuaderno en http://127.0.0.1:8888/lab/tree/work utilizando el token pasado en la variable de entorno JUPYTER_TOKEN.

Llamando al cuaderno desde la línea de comandos

Desde la línea de comandos, puedes descargar este pequeño script (requiere los paquetes requests y websocket-client) o ejecutarlo a través de un contenedor Docker:

# verificar la dirección IP de nuestro contenedor "testnb" previamente iniciadodocker inspect testnb | grep IPAddress            "SecondaryIPAddresses": null,            "IPAddress": "172.17.0.2",                    "IPAddress": "172.17.0.2",# Invocar nuestro cuaderno. Reemplaza la IP a continuación con la tuya obtenida en el paso anterior.docker run -it --rm \  -e JUPYTER_TOKEN=ab30dd71a2ac8f9abe7160d4d5520d9a19dbdb48abcdabcd \  tfoldi/jupyterapi_nbrunner 172.17.0.2:8888 /work/JupyterAPI_Test.ipynbCreando un nuevo kernel en http://172.17.0.2:8888/api/kernelsEnviando solicitudes de ejecución para cada celda{'data': {'text/plain': '15'}, 'execution_count': 3, 'metadata': {}}Procesamiento finalizado. Cerrando conexión websocketEliminando kernel

Este script se conecta a nuestro servidor de JupyterLab recién creado, ejecuta nuestro cuaderno, devuelve el resultado de la última celda y luego finaliza. Todo el procedimiento se realiza a través de protocolos web sin necesidad de realizar modificaciones en el código del cuaderno ni agregar bibliotecas adicionales.

Bajo el capó

Desafortunadamente, no hay un solo punto final en la API de Jupyter para ejecutar un notebook de principio a fin. Primero, tenemos que inicializar un nuevo kernel (o usar uno existente), recuperar los metadatos del notebook, obtener todas las celdas de código y enviar una solicitud de ejecución para cada una de ellas.

Para recuperar los resultados, necesitamos escuchar los mensajes entrantes en el canal de WebSocket. Dado que no hay un mensaje de “fin de todas las ejecuciones de código”, tenemos que llevar un seguimiento manual de cuántos bloques de código enviamos para su ejecución y cuántos de ellos se han completado realmente contando todos los mensajes de tipo execute_reply. Una vez que todo haya terminado de ejecutarse, podemos detener el kernel o dejarlo en una etapa de inactividad para futuras ejecuciones.

El siguiente diagrama ilustra el flujo completo:

Pasos para ejecutar un cuaderno de Jupyter sobre la API Rest. Las acciones a nivel de cuaderno utilizan la API Rest, mientras que las invocaciones a nivel de celda se realizan en WebSockets. Imagen del autor.

Para mantenernos autenticados, tenemos que pasar el encabezado Authorization en todas las llamadas HTTP y WebSocket.

Si esto parece una cantidad algo excesiva de pasos solo para ejecutar un notebook, te entiendo. Estoy seguro de que sería útil implementar una función de nivel superior dentro de Jupyter Server para reducir la complejidad.

El script completo está aquí, listo para ser utilizado en tus aplicaciones.

Programar nuestro cuaderno en GCP de forma gratuita (casi)

Aunque hay muchas opciones para alojar un cuaderno, la forma más rentable es aprovechar el servicio Cloud Run de Google Cloud. Con Cloud Run, solo pagas por el tiempo de ejecución exacto de tu trabajo, lo que lo convierte en una opción rentable para tareas que se activan con poca frecuencia sin paquetes adicionales o proveedores de SaaS adicionales (aparte de Google), y nuevamente, sin escribir ni una sola línea de código.

La arquitectura y el flujo de invocación se verán así:

Vamos a utilizar servicios sin servidor solo para mantener los costos bajos. Imagen del autor.

Primero, tenemos que implementar nuestro cuaderno en GCP Cloud Run. Hay varias formas de agregar un archivo a un servicio de Cloud Run, pero tal vez la más fácil sea copiar nuestros cuadernos en un contenedor Docker.

# Dockerfile simple para alojar cuadernos en un servidor JupyterFROM jupyter/base-notebookCOPY JupyterAPI_Test.ipynb /home/jovyan/workspaces/

Para compilar y hacer que el contenedor esté disponible en Cloud Run, simplemente podemos especificar la opción --source con gcloud run deploy, apuntándola a un directorio donde se encuentren nuestros cuadernos y el archivo Dockerfile.

# obtener el código fuente del cuaderno de Jupyter y el Dockerfilegit clone https://github.com/tfoldi/jupyterapi_nbrunner.git# Implementar el cuaderno de prueba en un contenedor jupyter/base-notebook # Los archivos Dockerfile y JupyterAPI_Test.ipynb están en la carpeta tests/test_notebookgcloud run deploy test-notebook --region europe-west3 --platform managed \  --allow-unauthenticated --port=8888 \  --source tests/test_notebook \  --set-env-vars=JUPYTER_TOKEN=ab30dd71a2ac8f9abe7160d4d5520d9a19dbdb48abcdabcd   [...]El servicio [test-notebook] revisión [test-notebook-00001-mef] se ha implementado y está atendiendo el 100 por ciento del tráfico.URL del servicio: https://test-notebook-fcaopesrva-ey.a.run.app

JupyterLab estará disponible en la URL del servicio. Google Cloud Run proporcionará los certificados SSL y los mecanismos para iniciar o suspender el contenedor según las solicitudes que lleguen a la implementación.

Para activar nuestra libreta recién implementada desde Cloud Scheduler, debemos crear una Función en la Nube vinculada a un tema de PubSub. El siguiente comando implementará main.py y requirements.txt desde este repositorio. El main.py es el mismo script que usamos anteriormente para activar nuestro código desde la línea de comandos.

# asegúrese de estar en el mismo directorio donde clonó los # contenidos de 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

Probemos nuestra nueva Función en la Nube enviando un mensaje al tema t_nbtrigger con los parámetros adecuados, tal como lo hicimos en la línea de comandos:

gcloud pubsub topics publish t_nbtrigger \  --message="test-notebook-fcaopesrva-ey.a.run.app:443        /workspaces/JupyterAPI_Test.ipynb --use-https"

Si revisas los registros de la Función en la Nube nbtrigger, podrás notar que emitir un registro al tema activó exitosamente la ejecución de la libreta que especificamos:

Los registros muestran la ejecución exitosa de nuestra libreta. Imagen del autor.

El último paso es crear una programación que se ejecute en momentos específicos. En este caso, estamos a punto de ejecutar nuestra libreta cada hora:

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"   

Listo, acabas de programar tu primera libreta de Jupyter de forma serverless.

CloudRun apaga automáticamente nuestro contenedor después de la ejecución del trabajo. El estado “inactivo” también es gratuito en caso de que no especifiquemos min-instances.

Nuestra libreta solo consumirá unos pocos centavos al día, lo que hace que este método de implementación sea uno de los más rentables en Google Cloud.

Después de unos días de ejecución, los costos son de alrededor de tres centavos.

Conclusión

Solíamos depender de convertir nuestras libretas de Jupyter en código Python para que estuvieran disponibles en herramientas nativas de la nube, o en servicios más complejos y costosos como Vertex AI o SageMaker. Sin embargo, al utilizar la API de Jupyter Rest y implementar tus libretas junto con su “entorno de desarrollo”, puedes omitir los pasos adicionales y habilitar llamadas de servicio web o programación para tus libretas.

Aunque este enfoque no es necesariamente apropiado para proyectos a gran escala con libretas de larga duración y uso intensivo de cómputo, es perfectamente adecuado para la automatización del hogar o proyectos de pasatiempo, sin gastar (demasiado) en infraestructura.