TL;DR Es posible usar Cloud Run para deployar una aplicación en Shiny, aunque si necesitas 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: https://demo.hasselpunk.com/rRofex y aquí está el código en GitHub
La idea con la que arranqué a esribir 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.
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 incluído. Voy a mantener la decisión de no usar rocker/shiny por dos motivos: primero, esta basada en Debian y en producción uso Ubuntu y segundo, me gusta aprender a hacerlo desde cero para entender que 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?
Aprvechando 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.
La aplicación que voy a subir es la que armé para una presentación de la libraría rRofex.
No es el objetivo de este posteo explicar en que consite, 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 GiHub, 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"]
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.
No voy a entrar en detalles en el paso a paso, porque tengo pendiente otro posteo al respecto, pero si voy a mostrar el resultado final y el código.
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):
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á todas 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……
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 impedimiento, en mi caso particular, la aplició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
Este punto no lo tenía en mis planes, aunque fuese algo que quería hacer desde hace un tiempo, no pensé hoy sería el día… 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 replicas de un contenedor distribuído 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, aquí use todos los parámetros por defecto que me indicaba Google Cloud, y luego tuve que 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 https://demo.hasselpunk.com/rRofex 😍
PD: si este blog te sirvió, me podes invitar un café con el siguiente botón!
PD2: Cambie la ubicación de la APP, en este momento no esta 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!