TL;DR: Es posible usar Cloud Run para deployar una aplicación en Shiny, aunque si necesitás websocket tendrás que dar una vuelta más y usar Cloud Run for Anthos para subir la aplicación a un cluster de Kubernetes. Esta es mi aplicación: demo.hasselpunk.com/rRofex y aquí está el código en GitHub.
La idea con la que arranqué a escribir este post fue ir mencionando las cosas que fui haciendo para lograr correr una aplicación de Shiny en Google Cloud usando Cloud Run en vez de levantar una máquina virtual que contenga al servidor.
Hasta el momento, me he encontrado trabajando con Cloud Run para subir APIs y, en conjunto con Cloud Scheduler para automatizar operaciones, pero esta fue mi primera vez usándolo con Shiny.
Alguna terminología
- Cloud Build: es un servicio que se usa para definir el flujo de trabajo a partir de eventos, por ejemplo: cuando se actualiza un repositorio en GitHub (una acción) se ejecuta la creación de una imagen (mediante un Dockerfile) que se deploya posteriormente en Cloud Run.
- Cloud Run: es una plataforma que te permite gestionar contenedores y abstraerte de la infraestructura donde están corriendo.
- Cloud Scheduler: es un servicio de Google que te permite planificar tareas, como por ejemplo, ejecutar un proceso que esté corriendo en Cloud Run.
- Cloud Run for Anthos: es una plataforma que te permite integrar Cloud Run dentro de un entorno de Kubernetes.
- Kubernetes: es una plataforma que permite orquestar múltiples contenedores entre clusters de máquinas virtuales.
El camino
Antes de iniciar la travesía, me propuse el siguiente plan de acción:
- Crear un nuevo repositorio en DockerHub que contenga una nueva imagen basada en augustohassel/r_ubuntu con Shiny incluido. Voy a mantener la decisión de no usar rocker/shiny por dos motivos: primero, está basada en Debian y en producción uso Ubuntu y segundo, me gusta aprender a hacerlo desde cero para entender qué estoy haciendo!
- Armar un Dockerfile que exponga una aplicación de Shiny y que funcione localmente usando como base la imagen que creamos en el punto anterior.
- Organizar un archivo de configuración (cloudbuild.yaml) que cree automáticamente el recurso en Cloud Run.
- Listo! … listo?
1. El nuevo repositorio en DockerHub
Aprovechando esta situación armé un nuevo repo que contiene también el tidyverse. La decisión de cargarlo en esta etapa, si bien es discutible, la tomé porque normalmente en cualquier aplicación de Shiny que armo trabajo con el tidyverse y, si tuviese que instalarlo al momento de crear la imagen de la aplicación, tardaría una eternidad.
Ya he escrito anteriormente sobre el proceso que me llevó a armar mi primer repositorio en DockerHub, así que están invitados a leerlo.
Por lo pronto, la imagen la van a poder encontrar aquí: r-shiny en Docker Hub.
2. Dockerizando Shiny
La aplicación que voy a subir es la que armé para una presentación de la librería rRofex.
No es el objetivo de este posteo explicar en qué consiste, pero si quieren replicarla localmente pueden hacerlo rápidamente con el siguiente Gist dentro de RStudio:
shiny::runGist(gist = "https://gist.github.com/augustohassel/4eea614f80a8bbc548b2b4c3c5edd7c3")
El Dockerfile que permite correr localmente la aplicación lo van a encontrar en GitHub, junto con el resto de los archivos de este posteo: GitHub Demo-ShinyDockerRun.
FROM augustohassel/r-shiny
# Cargo librerias
RUN R --slave -e "install.packages('remotes')"
RUN R --slave -e "remotes::install_github('rstudio/httpuv')"
# Llevo la aplicacion
RUN mkdir -p /srv/shiny-server
RUN rm -rf /srv/shiny-server/*
COPY app /srv/shiny-server/app
# Modifico la configuracion
COPY shiny-server.conf /etc/shiny-server/shiny-server.conf
# Creo el ejecutable
COPY shiny-server.sh /usr/bin/shiny-server.sh
RUN chmod +x /usr/bin/shiny-server.sh
RUN sudo usermod -a -G staff shiny
RUN mkdir -p /home/shiny/paquetes
COPY paquetes.R /home/shiny/paquetes/
RUN Rscript /home/shiny/paquetes/paquetes.R
EXPOSE 8080
CMD ["/usr/bin/shiny-server.sh"]
3. A la nube!
Ahora que tenemos todo funcionando localmente, es el momento de probarlo en la nube y para esto vamos a usar Cloud Run que es una solución “serverless” de Google.
Primero tenemos que crear un disparador con Cloud Build y asociarlo con nuestro repositorio de GitHub:
Para que todo funcione, el repositorio tiene que contener los siguientes archivos (además de la aplicación propiamente dicha):
- Dockerfile: secuencia para construir la imagen que se usará en el contenedor.
- cloudbuild.yaml: contiene los pasos que va a usar Cloud Build para deployar la aplicación.
- shiny-server.conf: configuración del server de Shiny.
- shiny-server.sh: ejecutable que arrancará la aplicación de Shiny.
Ahora, cada vez que actualicemos nuestro repositorio en GitHub, se van a ejecutar las tareas que se encuentren en el archivo cloudbuild.yaml:
Para subir la aplicación a Cloud Run, tenemos dos formas: la primera es manualmente en Cloud Run y la segunda proporcionando el archivo cloudbuild.yaml que contendrá todos los pasos para que se ejecute por sí solo!
# cloudbuild.yaml
steps:
# build the container image
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-f', 'Dockerfile', '-t', 'gcr.io/augusto-hassel/demo-shiny', '.']
# push the container image to Container Registry
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/augusto-hassel/demo-shiny']
# Deploy container image to Cloud Run
- name: 'gcr.io/cloud-builders/gcloud'
args:
[
'run',
'deploy',
'demo-shiny',
'--image',
'gcr.io/augusto-hassel/demo-shiny',
'--region',
'us-central1',
'--platform',
'managed',
'--quiet',
'--allow-unauthenticated'
]
images:
- gcr.io/augusto-hassel/demo-shiny
timeout: 14400s
Si llegaron hasta acá, entonces la aplicación probablemente les ha funcio… no…
4. Lis… Alerta! Calzada resbaladiza…
Cloud Run no soporta Websocket y lastimosamente mi aplicación lo necesita para funcionar…
Si bien Shiny Server posee otros protocolos que logran ‘bypasear’ este impedimento, en mi caso particular la aplicación lo usa para obtener información de otra API.
Si no fuese por este detalle, se podría haber resuelto según sugieren en el issue cargado al repo oficial de Shiny en GitHub.
Una luz de esperanza: Docker Run for Anthos 🚀
Por lo que estuve investigando, websocket está soportado si uno corre Cloud Run en un entorno de Kubernetes, así que manos a la obra!
¿Qué es Kubernetes? Bueno, en simples palabras es una plataforma que administra los contenedores de los cuales venimos hablando. Esto nos va a permitir, por ejemplo, tener réplicas de un contenedor distribuido en varias máquinas, las cuales se pueden autoescalar dependiendo del tráfico y el procesamiento que estén necesitando.
Para que funcione en Kubernetes primero tuvimos que crear un cluster de máquinas (usé los parámetros por defecto de Google Cloud) y luego habilitar algunos parámetros según me fueron requiriendo en Cloud Run (habilitar Cloud Run for Anthos por ejemplo).
Finalmente, tuvimos que modificar el YAML ligeramente para ejecutar la imagen en el cluster que recién creamos:
# cloudbuild.yaml
steps:
# build the container image
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-f', 'Dockerfile', '-t', 'gcr.io/augusto-hassel/demo-shiny', '.']
# push the container image to Container Registry
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/augusto-hassel/demo-shiny']
# Deploy container image to Cloud Run
- name: 'gcr.io/cloud-builders/gcloud'
args:
[
'run',
'deploy',
'demo-shiny',
'--image',
'gcr.io/augusto-hassel/demo-shiny',
'--cluster',
'my-first-cluster-1',
'--cluster-location',
'us-central1-c',
'--platform',
'gke'
]
images:
- gcr.io/augusto-hassel/demo-shiny
timeout: 14400s
Lo último fue seguir el siguiente tutorial para que se pueda acceder con un dominio personalizado: habilitar las conexiones HTTPS y los certificados TLS automáticos.
El resultado queda a la vista:
Mi primer aplicación de Shiny corriendo en Kubernetes: demo.hasselpunk.com/rRofex 😍
PD: si este blog te sirvió, me podés invitar un café con el siguiente botón!
PD2: Cambié la ubicación de la APP, en este momento no está corriendo en Kubernetes para disminuir costos únicamente, pero es válido todo el posteo si es que quieren colocar su propia aplicación en Kubernetes!