scieee AI-readable full text Open interactive document viewer

SharePC: Portal colaborativo para compartición de recursos remotos mediante WebRTC basado en Kubernetes, Vagrant y Electron-js.

Ortega Luis, Denis

Abstract

La tecnología se ha convertido en una parte central de nuestras vidas, y prueba de ello es la gran cantidad de software que se utilizan hoy en día. Sin embargo, con el paso del tiempo, este software se ha vuelto cada vez más complejo y necesita de más recursos hardware, hasta el punto en el que se necesitan superordenadores para ejecutar ciertos programas. Si bien casi todo el mundo dispone de un ordenador con potencia suficiente para realizar tareas básicas, no todo el mundo dispone de un equipo con potencia suficiente para ejecutar tareas demandantes como renderizar un vídeo o entrenar una red neuronal. ¿Habría alguna forma de realizar estas tareas sin necesidad de adquirir un nuevo ordenador? En este documento se propone una solución a este problema, mediante el desarrollo de una aplicación que permite sus usuarios ofrecer sus recursos al resto del mundo para que cualquier persona pueda usarlos de manera remota y segura. Para llevarla a cabo, se ha desarrollado una aplicación de escritorio y se ha configurado un servidor que hace uso de numerosas tecnologías de virtualización.

Full text

Equation Chapter 1 Section 1 Trabajo Fin de Máster Máster Ingeniería de Telecomunicaciones SharePC: Portal colaborativo para compartición de recursos remotos mediante WebRTC basado en Kubernetes, Vagrant y Electron-js. Autor: Denis Ortega Luis Tutor: Juan Manuel Vozmediano Torres Dpto. Teoría de la Señal y Comunicaciones Escuela Técnica Superior de Ingeniería Universidad de Sevilla Sevilla, 2013 iii Trabajo Fin de Máster Ingeniería de Telecomunicación SharePC: Portal colaborativo para compartición de recursos remotos mediante WebRTC basado en Kubernetes, Vagrant y Electron-js. Autor: Denis Ortega Luis Tutor: Juan Manuel Vozmediano Torres Dpto. de Ingeniería Telemática Escuela Técnica Superior de Ingeniería Universidad de Sevilla Sevilla, 2024 v Trabajo Fin de Máster: SharePC: Portal colaborativo para compartición de recursos remotos mediante WebRTC basado en Kubernetes, Vagrant y Electron-js. Autor: Denis Ortega Luis Tutor: Juan Manuel Vozmediano Torres El tribunal nombrado para juzgar el Proyecto arriba indicado, compuesto por los siguientes miembros: Presidente: Vocales: Secretario: Acuerdan otorgarle la calificación de: Sevilla, 2024 El Secretario del Tribunal vii Agradecimientos Me gustaría agradecer a mi familia y amigos por haberme apoyado durante todo este tiempo. Sin ellos habría perdido la motivación por este proyecto hace mucho y sin duda el resultado habría sido uno completamente distinto. También me gustaría agradecer a mi tutor en este trabajo por haberme guiado y aconsejado durante la realización del mismo. ix ÍNDICE DE TABLAS Tabla 1: CU-01 14 Tabla 2: CU-02 15 Tabla 3: CU-03 16 Tabla 4: CU-04 17 Tabla 5: CU-05 18 Tabla 6: CU-06 19 Tabla 7: CU-07 19 Tabla 8: CU-08 20 Tabla 9: CU-09 20 Tabla 10: CU-10 21 Tabla 11: CU-11 22 Tabla 12: Puertos abiertos en el router del master. 36 Tabla 13: Tabla de conexiones del servidor de señalización 43 xvii TABLA DE IMÁGENES Imagen 1: Diagrama de caso de uso de Autenticación del usuario 14 Imagen 2: Caso de uso de gestión y uso de entornos remotos 17 Imagen 3: Diagrama de casos de uso del host 21 Imagen 4: Esquema virtualización 24 Imagen 5: Diagrama cluster kubernetes 24 Imagen 6: Funcionamiento básico WebRTC 26 Imagen 7: Logotipo Electron-js 27 Imagen 8: Diagrama de la solución 30 Imagen 9: Configuración servidor para cri-o 35 Imagen 10: Configuración de dirección IP en Kubelet 36 Imagen 11: Comando de arranque de maestro 36 Imagen 12: Comando para configurar kubeconfig. 37 Imagen 13: Fichero de configuración de interfaz de red del worker. 40 Imagen 14: script de inicio del worker 41 Imagen 15: Recursos API Rest 46 Imagen 16: Tablas Base de Datos 51 Imagen 17: Diagrama API Rest 52 Imagen 18: Captura primera versión de aplicación RDP. 54 Imagen 19: Pantalla de Cliente 58 Imagen 20: Modal creación de contenedor. 59 Imagen 21: Modal de edición de contenedor. 59 Imagen 22: Ventana de conexión con entorno remoto. 60 Imagen 23: Ventana de conexión entrono remoto terminal. 61 Imagen 24: Pantalla de Host 62 Imagen 25: Modal de edición de máquina virtual 62 Imagen 26: Pantalla de inicio de sesión 64 Imagen 27: Estado botones 65 Imagen 28: Logotipo SharePC 65 Imagen 29: Diagrama de compartido de recursos 67 Imagen 30: Diagrama de solicitud de recursos 68 Imagen 31: Diagrama de unión a entorno remoto 70 Imagen 32: Diagrama de Eliminado de recursos Remotos 71 Imagen 33: Subida de imagen de nodo worker 83 Imagen 34: Pantalla de creación de cuenta. 85 Imagen 35: Modal de ajustes. 86 Imagen 36: Ventana principal aplicación. 86 Imagen 37: Formulario de creación de contenedor. 87 Imagen 38: Cartel informativo sobre la creación del contenedor 87 Imagen 39: Contenedor creado. 88 Imagen 40: ventana de edición de contenedor. 88 Imagen 41: Ventana de conexión de escritorio remoto 89 Imagen 42: Mensaje de alerta de instalación de Vagrant. 90 Imagen 43: Instalación Vagrant. 90 Imagen 44: Ventana de compartir recursos. 91 1 INTRODUCCIÓN 1.1 Motivación La tecnología se ha convertido en una parte central de nuestras vidas, y prueba de ello es la gran cantidad de software que se utilizan hoy en día. Desde aplicaciones móviles que nos permiten conectar con los demás, hasta el mundo empresarial con programas que facilitan la gestión de grandes empresas, es difícil encontrar una situación en la que no se use ningún programa informático. Sin embargo, con el paso del tiempo, este software se ha vuelto cada vez más complejo y necesita de más recursos hardware, hasta el punto en el que se necesitan superordenadores para ejecutar ciertos programas. Si bien casi todo el mundo dispone de un ordenador con potencia suficiente para realizar tareas básicas como edición de documentos o navegar por la web, no todo el mundo dispone de un equipo con potencia suficiente para ejecutar tareas demandantes como renderizar un vídeo o entrenar una red neuronal. Y aunque poseamos de esa potencia, no siempre vamos a estar aprovechándola al 100%. ¿Y si únicamente necesitamos realizar un cálculo puntual? ¿Tenemos que gastarnos miles de euros en un ordenador para aprovechar su rendimiento un par de horas? Me encontré en una situación similar poco antes de empezar con este trabajo. Un amigo mío quería probar un videojuego, pero éste requería de gran cantidad de recursos, los cuales su ordenador no poseía, y no pensaba comprarse un equipo nuevo únicamente para probar un videojuego. En mi caso, dispongo de un equipo bastante potente que me permite hacer casi cualquier cosa con él, así que, aplicando mis conocimientos en telecomunicaciones, conseguí crear una máquina virtual en mi ordenador, con la que mi amigo podía conectarse y jugar en remoto cuando quisiera. Esta situación me dio que pensar. ¿Y si hubiese alguna forma de poder compartir los recursos de tu ordenador de manera segura, y que puedan ser utilizados por quien los necesite? ¿Se podría llevar mi solución casera a una gran cantidad de usuarios? Decidí entonces hacer uso de mis conocimientos para llevar esta idea a la realidad. 1.2 Objetivos Pese a que tenía en mente una vaga idea sobre qué quería hacer, era necesario definir exactamente qué se pretendía hacer para más tarde comenzar la fase de diseño. A la hora de definir el objetivo, se tuvo mente la visión del usuario final. El objetivo final de este proyecto es el siguiente: una aplicación que permita a cualquier persona compartir sus recursos hardware, y a su vez, acceder a los recursos hardware compartidos por otra persona. Adicionalmente. este objetivo está sujeto a una serie de condiciones: • Cualquier persona, sin importar qué conocimientos tenga de informática, debe poder ofrecer sus recursos a cualquier persona que lo necesite. Esta persona es la que llamaremos como host. • Cualquier persona, sin importar sus conocimientos previos, debe poder acceder a los recursos que solicite. Este actor lo llamaremos cliente. • Los datos del host deben estar completamente aislados de los clientes. Ningún cliente podrá obtener los datos de la máquina cuyos recursos está usando. • Los datos del cliente deben estar completamente aislados. El host nunca podrá acceder a los datos del cliente que está haciendo uso de sus recursos. • El host debe poder usar su ordenador libremente mientras está compartiendo sus recursos, es decir, puede decidir compartir parte de sus recursos y utilizar el resto de potencia para uso personal. • Por motivos personales, se añade una condición extra, y es que se dependa lo mínimo posible de Introducción 12 12 servicios externos. Si se quiere desplegar algo en un servidor, se debe utilizar mi servidor propio. Esto es porque me apetecía trabajar directamente con un servidor para mejorar mis conocimientos sobre Linux y administración de servidores. Realizando un breve análisis previo, el objetivo se puede dividir en una serie de cuestiones a resolver. • Host multiusuario. ¿Cómo puede el host ofrecer sus recursos a cualquier persona mientras utiliza su ordenador de manera normal? • Acceso a los recursos. ¿Cómo puede el cliente acceder a los recursos ofrecidos por el host, de manera que parezca que está usando su propio equipo? • Seguridad de los datos. ¿Cómo se puede garantizar la seguridad de los datos tanto del cliente como del host? • Escalabilidad. La aplicación debe ser accesible por muchos usuarios y debe por ende ser escalable bajo demanda. • Equipos no controlados. Al tratarse de equipos de uso doméstico, pueden apagarse y encenderse en cualquier momento, y es imposible tener ningún control sobre esto. • Diversidad de usuarios. Los usuarios que usan la aplicación pueden tener o no conocimientos de redes o informática. No se le puede exigir al usuario ningún tipo conocimiento previo sobre nada. Además, cada usuario tendrá un escenario diferente, con un equipo y escenario de red distinto. Es necesario que la aplicación sea accesible para la mayor cantidad de usuarios posible. Todos estos problemas deberán ser resueltos de alguna forma para que el proyecto se considere un éxito. 1.3 Relación de medios materiales Para el desarrollo de este proyecto, se dispone de una serie de recursos hardware que serán utilizados a lo largo del proyecto. 1.3.1 Servidor Posiblemente la pieza más importante en todo el proyecto, el servidor. Se trata de un equipo que monté hace unos años y que, pese a no tener una gran potencia, es ideal para el desarrollo de este proyecto. Como es mi propio servidor, no he tenido que invertir tiempo configurando un servidor desde 0, puesto que ya tenía este preparado. Es cierto que podría desplegar todo en la nube, pero me apetecía probar a utilizar un servidor propio y realizar todo el proceso desde 0, puesto que así tenía más control sobre las partes del proyecto. Sus especificaciones son las siguientes: • Procesador Intel Xeon E5 2430 • 8Gb RAM DDR3 • SSD Samsung 250Gb • HDD 4Tb 1.3.2 Ordenador Principal Como es evidente, se ha utilizado un ordenador para desarrollar la aplicación y para hacer las primeras pruebas con máquinas virtuales. Sin duda lo más destacable es su procesador de 6 núcleos y sus 32Gb de RAM, que me han permitido ejecutar varias máquinas virtuales simultáneamente (hay veces que he llegado a usar 30Gb de RAM). 1.3.3 Ordenador secundario Además del ordenador principal, se ha utilizado un portátil para hacer pruebas en diferentes escenarios de red. 13 Esto ha sido esencial puesto que, si únicamente se hiciesen pruebas en la misma subred, no habría podido detectar fallos de configuración que únicamente permitían que la aplicación funcionase en la misma red local y no permitían el funcionamiento en otros escenarios. 1.4 Planificación de la ejecución Una vez se tiene el objetivo claro, se puede comenzar a trabajar en el proyecto. Para ello, se ha dividido en varias fases. • Análisis del problema. Lo primero es realizar un análisis del proyecto y ver qué problemas son necesarios resolver. • Investigación. Una vez se tienen claros los problemas, se ha realizado una labor de búsqueda de las posibles formas de resolverlos, así como ver sus puntos buenos y malos. • Diseño de la solución. Se comparan las alternativas existentes para resolver cada uno de los problemas y se eligen aquellas que se consideren mejores para resolverlo. • Implementación. Probablemente sea la tarea que más tiempo lleve, y se trata de desarrollar todo el software y realizar toda la configuración necesaria para que la aplicación funcione correctamente. • Pruebas. En todo momento se irán realizando pruebas para asegurar que todo vaya funcionando como se tiene en mente. 1.5 Estructura de la memoria Este documento se va a estructurar de la siguiente forma. • Análisis y diseño de la arquitectura. En esta sección se analizarán los problemas a resolver en este proyecto, y se propondrá la solución por la que se ha optado y que se desarrollará a lo largo del proyecto. • Implementación. Se mostrará el proceso seguido durante el desarrollo e implementación de los distintos componentes del proyecto. • Plan de pruebas. Conjunto de pruebas realizadas para verificar el correcto funcionamiento de todo el sistema. • Conclusiones y línea de mejora. Repaso de lo realizado en el proyecto y posibles aspectos a mejorar. Análisis y diseño de la arquitectura 14 14 2 ANÁLISIS Y DISEÑO DE LA ARQUITECTURA Como se ha mencionado anteriormente, se han realizado una serie de etapas previas al desarrollo e implementación del proyecto. En este apartado se describirá lo realizado en la etapa de análisis y diseño del sistema. 2.1 Casos de uso Antes de buscar soluciones a los problemas descritos anteriormente es necesario detallar, desde el punto de vista del usuario final, las operaciones posibles en el sistema a desarrollar. Esto se hará definiendo los posibles casos de uso generales a todo el proyecto. 2.1.1 Autenticación del usuario En esta sección se detallan los casos de uso relacionados con la autenticación del usuario en el sistema. Estos serán comunes tanto si el usuario es un cliente como un host. Imagen 1: Diagrama de caso de uso de Autenticación del usuario Tabla 1: CU-01 CU-01 Iniciar sesión en el sistema Precondición El usuario debe estar registrado en el sistema. CU-02 Descripción Proceso que debe seguir el usuario para iniciar sesión en el sistema Secuencia nominal Paso Acción 1 El usuario introduce su usuario y contraseña en la pantalla que se muestra al abrir 15 la aplicación de escritorio. 2 El usuario pulsa el botón de iniciar sesión. Postcondición El usuario accede al menú principal de la aplicación de escritorio. Excepciones Paso Acción E.1 El usuario introduce una combinación de usuario y contraseña erróneos. E1.1 Se informa al usuario que las credenciales son erróneas E.2 Se ha producido algún error de conexión al iniciar sesión. E.3.1 Se informa al usuario que se ha producido un error y que lo intente de nuevo más adelante. Comentarios - Tabla 2: CU-02 CU-02 Registrarse en el sistema Precondición Ninguna. Descripción Proceso que debe seguir el usuario para registrarse en el sistema Secuencia nominal Paso Acción 1 El usuario pulsa sobre el botón para registrarse en el sistema. 2 El usuario introduce un correo, un nombre de usuario y una contraseña en el formulario. 3 El usuario pulsa el botón de registrarse en el sistema. Postcondición La aplicación registrará al usuario en el sistema y lo devuelve a la pantalla de inicio de sesión. Excepciones Paso Acción E.1 El usuario introduce un correo o un nombre de usuario ya existentes en el sistema. E1.1 Se informa al usuario que ya existe una cuenta con ese nombre de usuario o correo electrónico asociado. Comentarios Actualmente no se envía ningún correo de confirmación, pero en caso de enviarse a producción, sería necesario añadir esta funcionalidad. Análisis y diseño de la arquitectura 16 16 Tabla 3: CU-03 CU-03 Cerrar sesión del sistema Precondición El usuario debe haber iniciado sesión en la aplicación. CU-01, CU-02. Descripción Proceso que debe seguir el usuario para cerrar la sesión del usuario en la aplicación. Secuencia nominal Paso Acción 1 El usuario clica en el botón de ajustes de la esquina superior derecha. 2 El usuario pulsa el botón de cerrar sesión. Postcondición La sesión se cierra en ese dispositivo y se muestra al usuario el formulario de inicio de sesión. Excepciones Ninguna. Comentarios Ninguno. 2.1.2 Gestión y uso de entornos remotos En este apartado se muestran los casos de uso que puede realizar el usuario final en caso de actuar como cliente, es decir, como usuario que solicita los recursos remotos. Se mencionan a continuación en múltiples ocasiones el término “recursos”, y se refiere a los elementos hardware que se solicitan como cliente y a los que más adelante se accederán. 17 Imagen 2: Caso de uso de gestión y uso de entornos remotos Tabla 4: CU-04 CU-04 Solicitar recursos Precondición El usuario debe haber iniciado sesión en la aplicación. CU-01, CU-02. El usuario debe encontrarse en la sección de “cliente”. Descripción Proceso que debe seguir el usuario cliente para solicitar recursos al sistema. Secuencia nominal Paso Acción 1 El usuario clica sobre el botón + 2 Se muestra un modal donde el usuario rellena los campos con los valores que desee de RAM, núcleos e imagen. Puede también dejar los valores por defecto. 3 El usuario pulsa sobre el botón de crear. 4 El recurso se crea satisfactoriamente y se muestra al usuario un mensaje indicándolo. Postcondición Se consultan los entornos remotos creados CU-06. Análisis y diseño de la arquitectura 24 24 Imagen 4: Esquema virtualización Tras pensarlo detenidamente se descartó la idea, puesto que además de que tardan bastante en arrancarse, sería necesario desarrollar muchas funcionalidades. Se tendría que mantener un registro de todas las máquinas virtuales abiertas, todos los hosts disponibles, decidir en qué host arrancar una máquina virtual basándome en los recursos que tuviese disponibles, etc. Sería en definitiva tratar de reinventar la rueda y llevaría demasiado tiempo cuando la duración del proyecto no podía ser de más de 3 meses. Existía una solución mejor y que ahorraría bastante tiempo, usar contenedores [2]. Los contenedores son iguales que las máquinas virtuales salvo que tardan mucho menos en arrancar, por lo que la idea no cambia: los clientes se siguen conectando a entornos virtuales en el host. Pero el uso de contenedores no solo reduce el tiempo de arranque del entorno, sino que además permite el uso de Kubernetes para gestionarlos. Con ello, uno puede despreocuparse del proceso de elegir el host en el que arrancar la máquina y todo lo que ello conlleva, puesto que de eso se encarga Kubernetes. Esto implica crear un cluster de Kubernetes, lo que se traduce en configurar el servidor como nodo maestro, configurar los hosts como nodos worker, asignar direcciones IP correctamente a los workers, etc. Pero en general esto mejorará la experiencia del usuario final reduciendo los tiempos de carga de los entornos virtuales, y a la larga me ahorraría tiempo al no tener que escribir código adicional (o eso pensaba yo). Imagen 5: Diagrama cluster kubernetes Al usar kubernetes, sigue siendo necesario usar una máquina virtual en el host (que se uniría al cluster de kubermetes como worker), y esto se debe a que los contenedores necesitan ejecutarse sobre el entorno Linux 25 (no necesariamente, pero no quería que el proyecto se complicase demasiado). Si se pretende que la aplicación sea usada por la mayor cantidad posible de usuarios, es obligatorio que sea posible utilizarse en Windows y en MacOS, por lo que la única solución es crear una máquina virtual que albergue un sistema operativo basado en Linux. Para automatizar el proceso de arrancado de las máquinas virtuales se ha decidido usar Vagrant [3]. Se podría haber utilizado la interfaz de comandos nativa de un virtualizador como VirtualBox o VMWare, pero para abracar la mayor cantidad de escenarios posibles se decidió usar Vagrant. Este se encarga de comunicarse con el virtualizador instalado en el equipo, que por defecto es VirtualBox aunque se pueden utilizar otros mediante complementos, por lo que parecía la solución idónea. Como Vagrant tiene compatibilidad por defecto con VirtualBox sin necesidad de extensiones adicionales, se utilizará este virtualizador para simplificar el desarrollo. La imagen de la máquina virtual que arrancará el host se almacenará en el repositorio de imágenes de Vagrant, Vagrant Cloud. Este permite únicamente subir imágenes públicas, por lo que habrá que tener cuidado con los datos personales que se almacenen en esta. Al final la idea quedó en lo siguiente, el host arranca una máquina virtual cuando decida ceder sus recursos. Esta máquina virtual se une al cluster de kubernetes cuyo nodo maestro se encuentra en el servidor, y los contenedores que se arranquen son a los que se conectan los clientes para usar los recursos hardware del host. En un principio tenía pensado intentar virtualizar la tarjeta gráfica para poder sacar mayor rendimiento, pero se descartó la idea al poco de empezar a investigar, puesto que, para poder usar la tarjeta gráfica en el contenedor, era necesario primero hacer un passthrough [4], que consiste en hacer una conexión directa entre la tarjeta gráfica y la máquina virtual del host. En primera instancia puede parecer que es algo sencillo, pero la realidad es que es un proceso complejo y que requiere usar Hyper-V o pagar por una versión mejorada de los virtualizadores disponibles. La primera opción requeriría usar Windows 10 Pro, la cual es una versión más cara de Windows y con ello estaría reduciendo el número de clientes que pueden usar la aplicación. La segunda, evidentemente, queda también descartada puesto que, de nuevo, reduce el número de clientes potenciales. Había alguna que otra opción adicional, como usar contenedores en WSL de Windows, pero se decidió dejar la opción de virtualizar la tarjeta gráfica como algo opcional, y desarrollarlo al final del proyecto si sobraba tiempo, aunque finalmente no se pudo trabajar en ello. 2.2.2 Comunicación cliente-host Este problema es otro de los principales de este proyecto, y ha determinado en gran medida el diseño final. Se trata de la conexión entre el cliente y su entorno virtualizado. Como se ha mencionado anteriormente, la idea que se tiene en mente es que el cliente solicite unos recursos y el primer host que tenga disponible esos recursos arranque un entorno virtual, y sea a éste al que accederá de manera remota el cliente. El problema de conexión remota a otros equipos es uno que existe desde el origen de Internet, y por ello hay muchas formas de solucionarlo. La más directa es abrir un servidor SSH el entorno remoto y conectarse a él mediante un cliente SSH desde una terminal. Si se desea en cambio una experiencia más cercana a la de un equipo doméstico, se puede instalar un servidor de escritorio remoto [5], y conectarse a un entorno de escritorio. Estas opciones funcionan perfectamente, pero dependen de que el servicio sea accesible públicamente, es decir, se pueda acceder directamente a la dirección IP y el puerto que abre el servicio. Una de las premisas de este proyecto es que todo el mundo pueda usar la aplicación, pero la realidad es que la mayoría de los usuarios tienen su ordenador detrás de un router haciendo NAT [6], lo que impide acceder directamente desde Internet a la máquina del usuario final. Una alternativa es que el usuario abra los puertos de su router, pero esto requeriría que tuviese conocimientos previos sobre informática y, como se planteó anteriormente, cualquier usuario sin importar sus conocimientos previos debería poder usar la aplicación. Para solucionar este problema se pueden utilizar técnicas como túneles SSH reversos [7], pero utilizando esta alternativa surge otro problema, y es que también se definió antes que tanto clientes como hosts se irían conectando y desconectando arbitrariamente, lo que se traduce en que estas conexiones cambiarán muy frecuentemente. Los túneles SSH funcionan muy bien si se configuran una vez y listo, pero no tenía del todo claro cómo iban a funcionar ante cambios en el escenario de red. Análisis y diseño de la arquitectura 26 26 Imagen 6: Funcionamiento básico WebRTC Tras un tiempo de búsqueda, me encontré con algo llamado WebRTC [8], y parecía ser exactamente lo que estaba buscando. WebRTC es una especificación orientada a conexiones en tiempo real entre dos o más aplicaciones, que permite transmitir vídeo y datos con muy poca latencia. En un primer momento, tenía la intención de establecer una conexión estilo SSH entre el cliente y el servidor porque, a priori, parecía más sencilla, pero viendo que WebRTC está orientado a la transmisión de vídeo, decidí realizar mi propia solución de escritorio remoto. Para el proyecto, el uso de WebRTC se traduce en que tanto el cliente como el entorno virtual en el host deben tener algún software adicional que permita esta conexión. Además, hace falta un servidor TURN/COTURN y un servidor de señalización [9]. • TURN/COTURN. Se decidió desplegar coturn [10], que no es más que una implementación de este tipo de servidores. Había más implementaciones existentes, pero esta era la más conocida. • Aplicación desarrollada. Ya se contemplaba la necesidad de desarrollar una aplicación de escritorio, por lo que era cuestión de añadir el uso de WebRTC. Adicionalmente, habría que desarrollar un pequeño programa que hiciese de cliente WebRTC dentro del entorno virtual que se arrancase en el host. • Servidor de señalización. Es un servidor que permita realizar la primera comunicación entre los dos participantes de la comunicación. En todos los ejemplos que había disponibles por Internet se utilizaba una conexión directa entre los dos participantes mediante websocket, lo cual no es posible en el caso de este proyecto porque los participantes se encuentran detrás de NAT. Otra opción era haciendo uso del servicio de base de datos de la cloud de Google Firecloud, pero como quería que todo lo relacionado con servidores se desplegase en mi servidor personal, decidí desarrollar mi propio servidor de señalización. Este se encargaría de hacer de relay entre el cliente y su entorno remoto para que puedan establecer una conexión WebRTC. Se decidió desarrollarlo en node.js, puesto que es uno de los lenguajes más utilizados para backend y porque, como se comentará a continuación, la aplicación de escritorio se desarrollará en javascript, por lo que utilizar el mínimo número de lenguajes posible 27 facilitará el trabajo. 2.2.3 Entorno remoto Los entornos remotos a los que se conectará el cliente son contenedores que albergarán un sistema operativo en su interior. Como se ha mencionado antes, los contenedores deben contener un software que permita la comunicación mediante WebRTC, y deben basarse en imágenes preexistentes. El cliente podrá seleccionar entre una selección de imágenes que contendrán todo el software a utilizar y, si lo necesita, podrá instalar todo el software que necesite en su entorno virtual, pero deberá hacer esto siempre que el contenedor se arranque de nuevo. Las imágenes en las que se basarán los contenedores se almacenarán en el repositorio de imágenes de Docker, Docker Hub. Todas ellas se almacenarán con el mismo nombre, y lo único que variará es el tag, que servirá para identificar cada una de las imágenes. La razón por la que se ha decidido hacer así es para mantener el repositorio en privado (puesto que la capa gratuita de Docker Hub únicamente admite una imagen privada). Esto conlleva sus limitaciones, las cuales se expondrán más adelante. Las imágenes que se creen deben soportar conexión de escritorio remoto, y adicionalmente, se añadirán otras imágenes que soporten esta conexión a través de una especie de conexión SSH. Para la imagen que soporte una conexión RDP incluye una serie de particularidades, como que, al necesitarse transmitir un escritorio, debe crearse un entorno de escritorio virtual (puesto que las imágenes de Docker no suelen tenerlas, al no tener físicamente un monitor conectado). Para poder capturar este escritorio y enviarlo mediante WebRTC, se necesita crear una aplicación de escritorio que consiga capturar este vídeo. La forma de realizar esta aplicación de escritorio se comentará en el siguiente punto. Para la imagen que se conecta mediante SSH, no hace falta todo el tema del escritorio remoto, por lo que con una simple aplicación de consola basta. Para esta se utilizará Node-js para utilizar el mínimo posible de lenguajes diferentes. 2.2.4 Aplicación de escritorio Para el desarrollo de la aplicación existían, de nuevo, muchas alternativas. Las aplicaciones de escritorio existen desde que existen los sistemas operativos con interfaz gráfica, por lo que hay mucho donde elegir. La forma en la que se ha decidido cómo desarrollar la aplicación es consultando aplicaciones que usaba en el día a día, que no tengan fallos de rendimiento y que me llamen la atención por su estética. En concreto, me fijé en Discord, VS Code y Parsec. Son aplicaciones cuya estética me gusta y que uso a diario. Tras consultar, vi que todas ellas estaban desarrolladas en Electron-js [11]. Resulta que Electron-js es usado en una gran cantidad de aplicaciones, y la principal razón es que permite una fácil reutilización del código. Se basa en empaquetar aplicaciones web en aplicaciones de escritorio, de manera que la aplicación final es una especie de navegador, pero con posibilidad de usar Node-js para tener un acceso a más bajo nivel. Esto permite que se pueda reutilizar prácticamente todo el código de una aplicación web y además permite empaquetarse para distintos sistemas operativos como Linux y MacOS. Es cierto que puede afectar algo al rendimiento, pero en la mayoría de situaciones es un precio aceptable por agilizar el desarrollo. Imagen 7: Logotipo Electron-js Análisis y diseño de la arquitectura 28 28 Un gran beneficio a la hora de usar Electron-js (y, en consecuencia, javascript, css y html) es que el estándar WebRTC tiene mucha documentación para javascript, hasta el punto que la documentación oficial muestra ejemplos desarrollados en este leguaje. Si bien se podría utilizar cualquier otro lenguaje, usar un lenguaje con extensa documentación agilizaría un poco el proceso. Quizás el único problema es que no tenía ninguna experiencia previa desarrollando en javascript, ni aplicaciones web en general, pero me parecía una buena ocasión de practicar un nuevo leguaje y ganar experiencia desarrollando aplicaciones web. Electron-js, pese a empaquetar una aplicación web, permite también el uso de comandos a más bajo nivel, gracias a que empaqueta también node-js. Con éste se pueden ejecutar comandos directamente en el sistema operativo, consultar el sistema de ficheros, etc. Todo esto es esencial para el desarrollo, de este proyecto. En este proyecto, se ha decidido desarrollar una aplicación común para clientes y hosts, que incluirá toda la lógica de gestión de recursos remotos, gestión de compartición de recursos y la unión mediante WebRTC a los entornos virtuales remotos. Por otro lado, también se desarrollará una pequeña aplicación que irá en los contenedores, y que permitirá establecer una conexión de escritorio remoto haciendo uso de WebRTC. Como se ha comentado anteriormente, es necesario que sea una aplicación de escritorio para que pueda capturar el entorno de escritorio virtual creado en el contenedor. 2.2.5 API Rest Por último, es necesario encontrar una solución para que todos los componentes se puedan comunicar entre ellos. La opción más usada en estos casos es una API Rest [12], la principal decisión a tomar es cómo desplegar esta API. En una primera instancia, decidí desarrollar la API en la cloud de Amazon, AWS, puesto que en mi TFG desarrollé una API Rest en esta plataforma. Sin embargo, esta solución iba en contra de una de las normas impuestas al principio, y es que, idealmente, se desplegarán todos los servicios en el servidor propio, sin depender de servicios externos. Además, se decidió usar Python 3.8 para implementar la API, pero esta versión dejaba de tener soporte en AWS en unos meses. Por ello, si se decidía usar esta versión, era necesario desplegarla en otro lugar. La razón por la que se usó Python 3.8 es que es la misma versión que la usada en la API desarrollada en mi TFM, y al usarla no sería necesario configurar ninguna librería de nuevo, lo que ahorraría algo de tiempo durante el desarrollo (que, dada la magnitud del proyecto, no venía nada mal). Por este motivo, se decidió desplegar la API en el servidor ejecutándose dentro de un contenedor. Cabe destacar que, en una situación ideal, la versión de Python a utilizar debe ser la última disponible, puesto que se asume que esta versión tendrá menos vulnerabilidades, pero se optó utilizar la versión 3.8 por ahorrar tiempo en el desarrollo del proyecto. En este proyecto, la API permitirá la comunicación de todos los componentes entre ellos. Cuando el cliente solicite ciertos recursos, lo hará a través de la API, y esta se encargará de avisar al maestro kubernetes (a través de su API). De igual manera, los contenedores se comunicarán con la API para indicar que ya se encuentran disponibles. Por último, el servidor de señalización hará uso de la API para comprobar si el origen y el destino tienen permiso para establecer una comunicación. Como es evidente, la API Rest necesitará almacenar información de alguna forma, pero estas no pueden almacenar información persistente, por ello, se necesita de un servicio externo que permita almacenar información. En este proyecto se ha decidido utilizar una base de datos PostgreSQL, puesto que, de nuevo, cuando se implementó la API en el TFG, se utilizó este servicio de base de datos. Además, el servidor contaba con este servicio instalado previamente, por lo que se ahorraría algo de tiempo al no tener que invertirlo en el despliegue y configuración de la base de datos. La estructura de tablas y datos utilizados se explicará en la sección de implementación. El servicio de base de datos está instalado en el propio sistema operativo, pero la API Rest se desplegará en un contenedor Docker. 2.2.6 Seguridad y escalabilidad Un último detalle que es necesario resaltar es que se ha tenido en cuenta que la escalabilidad y la seguridad a lo 29 hora de diseñar y desarrollar el proyecto. Esto significa que, en el diseño y desarrollo de todas las zonas del proyecto, se ha tenido en mente que todos los recursos serán accesibles desde Internet y por ello deben estar protegidos debidamente y poder escalar sin problemas. • API Rest. Al estar expuesta a Internet, es necesario asegurarse que únicamente puede ser utilizada por usuarios de la aplicación. Esto significa que todo usuario deberá autenticarse si quiere usar este servicio. Para ello, se hace uso de un usuario y una clave que permiten identificar al usuario, y una vez autenticado en la API, se devuelve un token que permite utilizarla sin necesidad de estar introduciendo la contraseña constantemente. Idealmente, estos tokens tienen un periodo de validez limitado y tendría que controlarse el acceso con ellos según si están o no caducados, pero en el proyecto no se tiene esto en cuenta, puesto que no considero que sea alcance de este proyecto la gestión de sesiones a largo plazo. Por otro lado, se ha procurado que los recursos sean accesibles únicamente por los usuarios correspondientes. Es decir, un usuario no puede acceder a los recursos de otro. • Servidor de señalización. Recordar que el servidor de señalización permite la comunicación mediante WebRTC entre el cliente y su contenedor. Cuando un usuario se quiere conectar a un entorno remoto, se comprueba si el usuario tiene permiso para establecer esta comunicación. Esto lo hace el servidor de señalización cuando recibe una petición de conexión, consultando a la API Rest que la sesión del usuario exista, y que cliente y contenedor tengan permiso para comunicarse. Solo en el caso en que tengan permiso, el servidor reenvía los mensajes entre cliente y contenedor. Adicionalmente, el servidor de señalización comprueba que todos los usuarios conectados estén operativos, de manera que, si ninguno de los actores de la comunicación está operativo, se eliminan todos los datos de la comunicación. Además, si por algún casual el contenedor cayese, cuando arrancase de nuevo se iniciaría la conexión automáticamente con los clientes. Esto hace que los apagados arbitrarios de hosts afecten lo mínimo posible al cliente y, además, que se ahorren recursos en el servidor de señalización al no mantener almacenadas conexiones que no se utilizan. • Kubernetes. En el maestro de kubernetes se mantiene un registro de los nodos trabajadores, estén o no disponibles. Por ello, cuando un host se desconecta o deja de compartir recursos, se indica mediante la API que se elimine este recurso del maestro, para que no lo tenga en cuenta a la hora de realizar los cálculos. Esto permite, de nuevo, ahorrar recursos en el servidor al liberarlo de recursos que, posiblemente, no se vuelvan a utilizar. 2.3 Descripción de la solución Teniendo claros los objetivos de este proyecto, y habiendo analizado detenidamente cada uno de los problemas a resolver, se llegó a la siguiente solución. A continuación, se muestra el diagrama con la solución planteada (Imagen 8). Análisis y diseño de la arquitectura 30 30 Imagen 8: Diagrama de la solución Como se puede apreciar en el diagrama, la solución se dividirá en distintas máquinas: servidor, host y cliente. A continuación, se detallará lo que se hará en cada uno de ellos. 2.3.1 Servidor Se corresponde con un ordenador accesible públicamente y que se encuentra constantemente encendido. En él se encontrarán varios programas en ejecución. Para otorgar un espacio aislado que contenga los datos del cliente en la máquina host, se hará uso de contenedores, y para facilitar el trabajo con ellos se utilizará Kubernetes, un orquestador de contenedores, cuyo nodo maestro se ejecutará en este servidor (1). Cuando el cliente solicite los recursos, se creará un contenedor en el equipo host que actúa de worker (2), al cual se conectará el cliente. Para poder establecer la conexión entre cliente y servidor, existen una gran variedad de opciones. El principal inconveniente de nuestro escenario es que no tenemos acceso a los routers de los clientes ni de los hosts, puesto que ello requeriría que los usuarios finales supiesen acceder a un router. Por ello, la solución por la que se ha optado es usar WebRTC para comunicar el cliente con su contenedor en el host. Esto se traduce en que, en el servidor, se ejecutará un Servidor de Señalización (3) y un servidor TURN y STUN (4). Los contenedores que se desplieguen llevarán una aplicación en su interior, que permitirá al cliente comunicarse con el contenedor. Finalmente, para que todos los componentes del escenario se puedan comunicar entre sí, se desplegará también una API Rest (5), la cual se valdrá de una base de datos PSQL (7) para almacenar los datos de manera persistente. El servidor en el que se desplegarán todos estos servicios es mi servidor particular, por lo que es importante que los elementos que se instalen se puedan eliminar fácilmente. Por ello, a excepción del maestro kubernetes, el 31 resto de servicios se encapsularán en contenedores Docker. 2.3.2 Host Se trata del ordenador que cede sus recursos. Esto lo hará a través de la aplicación de escritorio (6) que se comunicará con el resto del sistema. La aplicación arrancará una máquina virtual (2) cuando se quieran compartir los recursos, la cual que se configurará automáticamente, y se unirá al cluster de kubernetes. La razón por la que se decide crear una máquina virtual es, por un lado, para tener un entorno controlado y común en todos los hosts, y por otro, porque si se van a ejecutar contenedores, es necesario que el sistema operativo sobre el que se ejecute sea Linux. A partir de ese momento, mientras la máquina virtual siga funcionando, el host estará compartiendo los recursos. Otra ventaja de utilizar una máquina virtual es que el host puede especificar cuántos recursos compartir asignando cierta cantidad de memoria RAM y CPU a la máquina virtual. 2.3.3 Cliente Se corresponde con el ordenador del usuario que solicita el alquiler de recursos. Se trata de un ordenador común con Windows, aunque es relativamente sencillo adaptarlo a un ordenador con Linux o MacOS. En este ordenador habrá instalado una aplicación de escritorio (6), la cual se encargará de comunicarse con el resto del sistema. Esta hará uso de la API Rest para solicitar los recursos que necesite el cliente, la cual le indicará al maestro kubernetes que arranque un contenedor en algún host disponible y establecerá la conexión con el contenedor mediante WebRTC. Una vez se haya establecido la conexión, permitirá al cliente controlar el contenedor destino ya sea por comandos o por una especie de escritorio remoto. 2.4 Limitaciones Evidentemente la solución propuesta no es perfecta y presenta sus limitaciones. Entre ellas, se pueden destacar las siguientes. • Rendimiento de los entornos remotos. La solución actual permite compartir la memoria RAM, disco duro y potencia del procesador. A priori puede parecer que es suficiente, pero una gran cantidad del rendimiento del ordenador es aportado por la tarjeta gráfica, y sería ideal que se pudiese compartir. Como se ha mencionado anteriormente, es posible compartir la potencia de la tarjeta gráfica, pero sólo lo podría hacer una parte de los hosts. Por limitaciones de tiempo, se ha decidido omitir esta parte, aunque sería cuestión de configurar el virtualizador y los contenedores para hacer uso de la tarjeta gráfica en los entornos virtuales. • Calidad de la conexión entre cliente y contenedor. Como se ha visto anteriormente, la conexión se establece mediante WebRTC, y permite una suerte de conexión de escritorio remoto con el contenedor. Por desgracia, al no disponer los contenedores de hardware específico de codificación de vídeo (tarjeta gráfica), la latencia es bastante notoria. A pesar de ello, se podría optimizar la comunicación para reducir la latencia lo máximo posible, pero se considera que este proyecto no se especializa en la compresión y transmisión de vídeo en tiempo real, por lo que queda fuera del alcance del mismo. • Sistema operativo del entorno virtualizado. Con la solución propuesta, los entornos virtualizados cuentan con Linux como sistema operativo, pero es entendible que algunos usuarios prefieran utilizar Windows. No estoy del todo seguro si esto es posible o no, por lo que habría que realizar una labor de investigación. Podría ser una muy buena opción, pero quería centrar este proyecto en montar una primera solución funcional y la opción más directa era usar Linux en los sistemas remotos. • Almacenamiento de datos en el entorno virtualizado. En la solución actual hay una limitación bastante grande, y es que los datos del cliente permanecen en todo momento en el equipo del host, por lo que, si se pierde la conexión con éste, se pierden también los datos. Esto evidentemente está lejos de ser ideal, por lo que habría que buscar una forma de mantener los datos fuera de la máquina del host. En una primera instancia pensé en una carpeta remota compartida alojada en el ordenador del cliente, Análisis y diseño de la arquitectura 32 32 pero se sigue teniendo el problema de que este ordenador no es accesible remotamente. Decidí dejar este problema para el final, puesto que es completamente independiente de la solución principal, y por falta de tiempo, acabé dejándolo fuera del proyecto. • Almacenamiento de imágenes. Pese a que una de las condiciones de este proyecto es que se dependa lo mínimo posible de servicios externos, se han utilizado repositorios de imágenes públicos. Esto es porque utilizar un repositorio privado de imágenes implicaría la configuración de más componentes en el servidor, y con ello, se alargaría el tiempo de este proyecto. No se ha considerado como algo fundamental, por lo que, para reducir la duración del proyecto, se ha optado por usar los repositorios públicos de Vagrant y de Docker. • Entorno remoto volátil. Cuando un cliente ingresa en su entorno virtual, puede instalar todas las aplicaciones que desee, pero en el momento en el que el contenedor que alberga su entorno cae (ya sea porque el equipo host se apague o por que el contenedor se corrompa), se pierden todas las cosas instaladas y habría que instalarlas de nuevo. Esto evidentemente está lejos de ser lo deseable por el usuario, puesto que por mucho que se suban imágenes con todo el software posible, sería imposible abastecer a todas las necesidades de los clientes y que todos los clientes tengan una imagen con exactamente aquello que necesitan. Una buena opción habría sido que el usuario pueda utilizar su propia imagen, puesto que, en la solución actual, el cliente solo puede utilizar una serie de entornos preexistentes, y si desea utilizar un software adicional, deberá instalarlo cada vez que el entorno se arranque. Esta idea en cambio conlleva el desarrollo de software adicional para gestionar las imágenes subidas por el usuario, la construcción de las propias imágenes, etc. Si bien hubiera sido una gran idea, por limitaciones de tiempo no se llegó a implementar. En resumen, el proyecto se puede dividir en las siguientes tareas: • Desarrollar una aplicación de escritorio. • Desarrollar un servidor de señalización. • Desplegar y configurar un cluster de kubernetes. • Configurar el servidor y arrancar todos los servicios necesarios. • Crear una máquina virtual que se una automáticamente al cluster. • Crear los contenedores que se arrancarán en los hosts. o Conexión terminal. o Conexión escritorio remoto. • Desarrollar y desplegar una API Rest. 2.5 Software utilizado Anteriormente se mencionó que se diseñó el proyecto teniendo en cuenta una serie de elementos hardware disponible. Además de esto, se han utilizado una gran cantidad de software, ya sea en forma de herramientas que han asistido al desarrollo del proyecto, o en forma de recursos y servicios de terceros implementados en la solución final. A continuación, se comentan las herramientas software utilizadas para el desarrollo, y el resto de software se irá comentando a lo largo del documento a medida que se vaya utilizando. 2.5.1.1 Visual studio code La herramienta principal utilizada para el desarrollo de los distintos componentes. Se trata de un editor de código desarrollado por Microsoft muy sencillo de utilizar y con una gran cantidad de funcionalidades ampliables mediante el uso de extensiones. En este proyecto, se ha utilizado para desarrollar la aplicación de escritorio, el 33 servidor de señalización, la API y el software que se ejecuta en los entornos virtuales. 2.5.1.2 Postman Se trata de una herramienta destinada a probar distintos servicios web. Inicialmente se diseñó para probar APIs, aunque con el tiempo ha ido evolucionando y permite probar websocket o brokers MQTT. En este proyecto se ha utilizado para hacer pruebas tanto de la API Rest como del servidor de señalización. Además, se ha utilizado para crear la API siguiendo la especificación OpenAPI y para documentarla. 2.5.1.3 Filezilla Es una herramienta que permite transmitir archivos mediante el protocolo SFTP. En este proyecto se ha utilizado para enviar al servidor todos los ficheros necesarios, como el código de las imágenes de Docker que se arrancan en su interior o todos los ficheros y scritps que permiten ejecutar y configurar los servicios que aloja. 2.5.1.4 Virtual Box y Vagrant Como se ha mencionado anteriormente, en el proyecto es necesario crear máquinas virtuales. Para ello se ha utilizado Vagrant, el cual a su vez hace uso de VirtualBox para arrancarlas. Se ha utilizado Vagrant tanto en la propia aplicación en sí, como forma de arrancar las máquinas virtuales en los Hosts, como en la fase de desarrollo para crear las máquinas virtuales utilizadas para probar y desarrollar distintos aspectos del proyecto. 2.5.1.5 Docker y Kubernetes En la solución planteada, se hace uso de kubernetes para gestionar los distintos contenedores que se irán creando en los hosts. Para crear las imágenes que se ejecutarán en los contenedores desplegados en los hosts se ha hecho uso de Docker y Docker Hub. Además, se ha utilizado Docker para la creación de imágenes y despliegue de contenedores en el servidor, de manera que se facilita el despliegue de servicios en el mismo. Gracias a la herramienta de kubectl se ha podido gestionar de una manera cómoda el cluster de kubernetes, y se han podido depurar correctamente los errores encontrados durante el desarrollo del proyecto. 2.5.1.6 Git y GitHub desktop El desarrollo de este proyecto se ha hecho en múltiples ordenadores, dado que era necesario realizar pruebas en distintos escenarios de red. Para poder compartir de manera sencilla este código y mantener una copia de seguridad en la nube, se ha hecho uso del repositorio de control de versiones GitHub, y en concreto, de su cliente con interfaz gráfica GitHub Desktop. Este permite operar con una gran facilidad sobre el repositorio y evita la necesidad de utilizar comandos. 2.5.1.7 PgAdmin4 Durante el proyecto ha sido necesario configurar y gestionar la base de datos PSQL. Para ello se ha usado pgAdmin 4, uno de los softwares diseñados para configurar y manejar bases de datos PostgreSQL más usado. Este software permite conectarse a una base de datos y administrarla. Dispone de una interfaz gráfica para crear, configurar y visualizar tablas, haciendo estas acciones más cómodas de realizar con respecto a un cliente por consola. 2.5.1.8 Diagrams.net Para la elaboración de algunos diagramas usados en este documento, se ha utilizado la aplicación web diagram.io, que permite realizar de forma rápida y sencilla diagramas de todo tipo. Además, viene con multitud de imágenes predefinidas que son de gran utilidad. 2.5.1.9 Carbon Para crear las imágenes que contienen código, se ha utilizado esta aplicación web. Permite realizar capturas de prácticamente cualquier lenguaje, así como personalizar los colores con los que aparece. Implementación de la solución 40 40 que se ejecuten en el equipo host, y se conectan al exterior a través de un router virtual haciendo NAT. Como las máquinas virtuales están ejecutando linux, basta con cambiar la dirección IP de la interfaz conectada a la subred virtual y añadir una ruta por defecto para el ruter virtual que hace de gateway. En la práctica, consiste en ejecutar el comando sudo netplan apply después de modificar el fichero de configuración de la interfaz, que en Ubuntu se suele encontrar en /etc/netplan/01-netcfg.yaml. Imagen 13: Fichero de configuración de interfaz de red del worker. En la captura anterior (Imagen 13) se puede apreciar el fichero de configuración que hay que copiar en la máquina virtual para configurar la dirección IP. En concreto, hay que modificar la línea 7, asignando la dirección IP correspondiente. En este proyecto se ha asignado el rango de direcciones 10.0.0.0/8, puesto que así se permite una gran cantidad de usuarios. Para la asignación de direcciones IP, se ha desarrollado una función en la API que permite asignar una dirección IP a una sesión. Además, deja libre la dirección 10.0.2.2, que es la dirección del router virtual que funciona de Gateway. Cuando la aplicación de escritorio arranca una máquina virtual, primero consulta la dirección IP que le corresponde a ésta y modifica el fichero de configuración. Después, una vez arranque la máquina virtual, se ejecuta el comando para aplicar la configuración y la dirección queda cambiada. Adicionalmente, es necesario configurar la dirección IP que reconoce kubernetes como la del nodo. Para ello se realiza algo similar a lo realizado en el master, modificando el fichero /etc/default/kubelet con la dirección IP correspondiente. Con estos cambios realizados, el worker se puede unir al cluster. 3.1.3.3 Unón y desconexión al cluster Como se ha mencionado anteriormente, el worker se une al cluster mediante el comando generado por el maestro, pero antes de ello se necesitan modificar una serie de parámetros como se acaba de comprobar. El proceso de unirse al cluster es sencillo, cada vez que se arranque la máquina virtual se modifica la dirección IP y se asigna el nombre del equipo correspondiente. Entonces se ejecuta el comando de unión y el nodo quedará unido al cluster. Ya se ha visto cómo se asigna el nombre del equipo, pero para aplicar la configuración de la interfaz de red y unirse al cluster es necesario ejecutar comandos dentro de la máquina virtual. Para ello se ha hecho uso de que Vagrant monta un directorio compartido con el host en el interior de la máquina virtual. Al arrancar la máquina virtual, la aplicación de escritorio crea un fichero de configuración para la interfaz y un Shell script para que aplique la configuración y ejecute el comando de unión; y se copian en la carpeta compartida. Después, se ejecuta el comando vagrant up con la flag --provision, de manera que al arrancar la máquina virtual se ejecutaría el script especificado en el archivo vagrantfile. 41 Imagen 14: script de inicio del worker En la anterior imagen (Imagen 14) se muestra el script que se ejecuta al arrancar la máquina virtual. En él, se modificará la línea 11 con la dirección IP asignada al nodo, y además se añadirá al final el comando de unión al cluster. Una vez ejecutado este script, el contenedor queda unido al cluster. A la hora de desconectar el worker del cluster, se intentó primero, anular lo realizado por el comando kubeadm join, y segundo, eliminar el nodo del cluster. Para lo primero se ejecuta el comando kubeadm reset -f, que devuelve el nodo al estado previo a unirse al cluster, cerrando todos los contenedores que se pudieran haber abierto. Esto se hacía justo antes de unirse al cluster, para eliminar toda la información previa que pueda tener el nodo. Por desgracia, la próxima vez que se arranca la máquina virtual, al no haberse ejecutado todavía el comando de reseteo, el nodo se une automáticamente al cluster con la misma configuración que tenía antes. Esto hace que, si en el periodo que ha estado apagado el nodo se ha unido otro y ha usado su dirección IP, el proceso de unión provocará un error. Por ello, para evitar problemas, se ha optado por no apagar el worker, si no directamente eliminarlo, de manera que siempre que se arranque el worker, parta del mismo estado. Para lo segundo, se ha desarrollado una función en la API Rest que llama a la Kube-API y le indica que elimine ese nodo. Esto último se realiza cada vez que se apaga el nodo o cuando se detecta que está apagado, para asegurar que el nodo no está consumiendo recursos del cluster y no se le asignen. Los detalles de cuándo y cómo se arrancan las máquinas virtuales se verán en la sección de la aplicación de escritorio, puesto que es ésta la que se encarga de hacerlo. Implementación de la solución 42 42 3.2 Servidor de señalización Como se mencionó en la introducción, para que se pueda realizar la comunicación entre el cliente y su entorno remoto, se hace uso de WebRTC, y este a su vez depende de una serie de servicios. El primero que se va a comentar es el servidor de señalización, que se trata de un canal de comunicación accesible desde Internet que utilizan los peers para realizar un diálogo previo y que les permite establecer una conexión directa entre ambos. Como se tiene que establecer una conexión bidireccional entre cliente y peer, se ha hecho uso de WebSocket. La idea es que los peers se conecten con el servidor mediante websocket, de alguna forma se autentiquen e indiquen con qué otro peer quieren comunicarse y se produzca un enlace entre ellos. Entonces, cualquier mensaje que envíe el peer al servidor será reenviado al otro peer. 3.2.1 Protocolo de comunicación La conexión mediante WebSocket es bastante sencilla de establecer, el servidor comienza a escuchar en un puerto, y cuando el peer establece una conexión a este puerto, se establece un canal de comunicación. A partir de este momento, el servidor puede escuchar a eventos de conexión, desconexión y recepción de mensajes, los cuales son cadenas de texto. Al ser el canal de comunicación tan simple, se ha especificado un protocolo de comunicación basado en mensajes en formato JSON. Cuando el servidor recibe un mensaje del peer, se procesa el mensaje siguiendo una estructura definida y en base a su contenido realiza una acción u otra. Los mensajes tienen los siguientes campos: • Tipo: El tipo de mensaje. En función del tipo, se esperarán unos campos u otros, y el comportamiento del receptor variará. Hay un total de 4 tipos. o Tipo 0: Mensaje normal. o Tipo 1: Mensaje de autenticación. o Tipo 2: Mensaje de desconexión. o Tipo 3: Mensaje de Keepalive. • Contenido: Este campo se envía únicamente en mensajes de tipo 0 y 1. • Destino: Se envía en mensajes de tipo 0 y 1. • Origen: Se envía en mensajes de tipo 0. • Token: Se envía en mensajes de tipo 1. • Destinos: Enviado en mensajes de tipo 1. Cada mensaje tiene una funcionalidad y conlleva un comportamiento distinto según si el receptor es el cliente o el servidor. 3.2.1.1 Tipo 0 Estos mensajes son generados por los peers y reenviados por los servidores al destino específico. El valor de los campos es el siguiente. • Tipo: 0 • Contenido: El mensaje que desee enviar el peer al otro peer. • Destino: El peer al que se desea enviar el mensaje. • Origen: El peer que envía el mensaje. Cuando el servidor recibe este mensaje, comprueba que el emisor esté autenticado en el sistema y que pueda enviar mensajes al peer destino. Si tiene permisos, se busca el canal de comunicación del peer destino y envía el mismo mensaje que se ha recibido, pero si no tiene permisos simplemente no envía nada y no genera ningún evento. 43 3.2.1.2 Tipo 1 Estos mensajes son los relacionados con la autenticación del usuario. Los mensajes tienen los siguientes campos: • Tipo: 1 • Token: Únicamente está presente cuando es enviado por el peer. • Destino: Presente solo cuando lo envía el peer, y además es opcional. • Contenido: Presente en la respuesta del servidor. • Destinos: Presente en la respuesta del servidor, y además es opcional. En el protocolo de mensajes, este mensaje es enviado primero por el peer cuando quiere autenticarse y después responde el servidor. Cuando el peer se autentica, envía su token de sesión y el destino al que desea enviar los mensajes en caso de saberlo. El servidor, al recibir el mensaje, comprueba que el token exista en el sistema, en cuyo caso almacena la conexión en el sistema. Únicamente se procesarán mensajes que vengan de una conexión almacenada en el sistema. Si el peer ha enviado un destino al que desea enviar, el servidor responde con “true” o “false” según pueda enviar o no al destino. Si no ha enviado nada, el valor del “Contenido” será siempre true, y además se añade al campo “Destinos” un array con los peers que hayan solicitado previamente enviar a este peer. Esto último se hace para que los contenedores, en caso de haberse cerrado y vuelto a abrir, puedan reestablecer la conexión con los clientes con los que estaban conectados. 3.2.1.3 Tipo 2 Este es el mensaje de desconexión, y es enviado por el peer cuando desea desconectarse del servidor. El servidor, al recibirlo, elimina todo rastro del peer. 3.2.1.4 Tipo 3 Como es algo ingenuo pensar que todos los peers enviarán el mensaje de desconexión, se ha desarrollado un mecanismo para comprobar si el peer sigue conectado. Se basa en enviar mensajes de tipo 3 a todos los peers de forma periódica, y cada vez que se envíe el mensaje se resta un contador individual a cada conexión. Cuando el peer responde a este mensaje, el contador se reestablece a su valor inicial, pero si llega a 0, se actúa igual que si el peer se hubiese desconectado. Esto es muy útil con los contenedores, que no tienen ningún control sobre cuándo se van a desconectar, sino que simplemente se apagan cuando lo indique el cluster. Para los clientes, permite eliminarlos en caso de que el usuario cierre la aplicación sin que le dé tiempo a desconectarse o de que se produzca un error. 3.2.2 Autenticación y autorización de peers Como se ha mencionado anteriormente, los peers que quieren utilizar el sistema, necesitan autenticarse previamente. Esto lo hacen con los mensajes de tipo 1, en los que envían el token de sesión. Cuando un peer envía un mensaje de autenticación, el servidor consulta a la API Rest si el token es válido y que puede comunicarse con el destino (actualmente se comprueba únicamente que pertenezcan al mismo usuario, pero podría estar bien añadir opciones para que otros usuarios se unan a tu contendor). Al recibir la respuesta de la API, el servidor, almacena la conexión en caso de que el token sea válido, y enlaza los peers destino y origen en una tabla. Esta tabla tiene le siguiente formato (Tabla 13): Tabla 13: Tabla de conexiones del servidor de señalización idOrigen => WebSocket Send: [id1, id2, id3…] Keepalive: 5 Esta tabla tiene los siguientes campos: • idOrigen: Identificador del peer. Si es un cliente es su identificador de sesión, y si es un contenedor, es Implementación de la solución 44 44 el identificador del mismo. • WebSocket: El objeto WebSocket que contiene la conexión con el peer. Se utiliza para enviar mensajes al peer. • Send: La lista de peers a los que tiene permiso para enviar mensajes. Tanto el peer origen como el peer destino tienen que referenciarse en este campo para que se permita una comunicación bidireccional. • Keepalive: Es el contador utilizado para considerar que el peer está conectado con el servidor. 45 3.3 API REST La API Rest es el componente encargado de intercomunicar todas las partes del proyecto. Como se mencionó en el punto 2, se ha desarrollado en python y se ha desplegado utilizando Flask. Entre las funcionalidades de la misma, se pueden destacar las siguientes: • Autenticación y gestión de usuarios del sistema. • Solicitud de recursos por parte del cliente. • Notificación del estado de los contenedores. • Autenticación y autorización en el servidor de señalización. 3.3.1 Formato de mensajes Como en cualquier API Rest, la comunicación con el servidor se realizará a través de peticiones HTTP. La API presenta una serie de recursos, y será posible acceder a ellos especificando una ruta con el formato “/Test/{recurso}”. Las solicitudes a estos recursos desencadenan acciones en el servidor, y varían según el método HTTP utilizado. Adicionalmente, los datos necesarios para procesar las peticiones se envían en el cuerpo de la petición HTTP, aunque en el caso del método GET y PUT, estos irán en la query como parámetros. En cuanto a la respuesta del servidor, su contenido se envía en formato JSON y varía en función la petición realizada, pero, a menos que se produzca un error durante el procesado de la respuesta, el servidor responderá con al menos dos parámetros: • code: un valor numérico que identifica el tipo de respuesta del servidor. En general, si no se ha producido ningún error, este campo tendrá un valor de 0, y si se produce algún error o la respuesta no es la más común, el valor de este campo variará. • description: contiene una descripción de la respuesta. Es una forma de facilitar el trabajo con la API, al describir de manera resumida el significado del código devuelto. Esto es algo que aprendí del TFG puesto que, a la hora de realizar pruebas, es más fácil entender la respuesta recibida si viene acompañada de un mensaje identificativo que tener que consultar el significado del código recibido. 3.3.2 Recursos y métodos Se han definido una serie de recursos a los que se accederá o modificarán mediante diferentes métodos HTTP. A continuación, se muestra una imagen en la que se resumen todos los métodos (Imagen 15). Implementación de la solución 46 46 Imagen 15: Recursos API Rest 3.3.2.1 Autenticación Para el proceso de registro y gestión de usuarios y sesiones se han utilizado 2 recursos: session y account. 3.3.2.1.1 Account El recurso account responde ante los métodos POST y DELETE. Cuando un usuario desee registrarse en el sistema, deberá enviar una petición POST con el siguiente formato: • user: nombre del usuario asociado a la cuenta. Debe ser único en el sistema. • contra: contraseña de la cuenta. • correo: correo electrónico asociado a la cuenta. Debe ser único en el sistema. Ante esto, el servidor crea un nuevo usuario en el sistema con los parámetros solicitados y responde con el siguiente formato: • code: podrá tener los siguientes valores: o 0: El usuario se ha creado correctamente. o 1: El nombre de usuario se encuentra en uso por otra cuenta. o 2: El correo electrónico está siendo usado por otra cuenta. • description: Mensaje de descripción que varía según el código devuelto. 47 Por el contrario, si el usuario desea eliminar una cuenta, debe enviar una petición DELETE al servidor, y éste eliminará la cuenta del sistema. El body de la petición debe tener el siguiente formato: • token: identifica la sesión actual del usuario. La respuesta del servidor tiene el siguiente formato: • code: podrá tener los siguientes valores: o 0: Si no se ha eliminado el usuario correctamente. o 1: El token aportado no pertenece a ninguna sesión activa. • description: Mensaje de descripción que varía según el código devuelto. 3.3.2.1.2 Session Este recurso, de nuevo, responde a los métodos POST y DELETE. Si el usuario quiere iniciar sesión en el sistema, enviará una petición POST con el siguiente formato: • user: nombre del usuario de la cuenta con la que se quiere iniciar sesión. • contra: contraseña asociada a la cuenta con la que se quiere iniciar sesión. La respuesta del servidor sigue el siguiente formato: • code: podrá tener los siguientes valores: o 0: Se ha iniciado sesión satisfactoriamente o 1: La combinación de usuario y contraseña no existen • description: Mensaje de descripción que varía según el código devuelto. • token: Código que servirá para acceder al resto de funcionalidades de la API. Este valor estará asignado únicamente si no se produce ningún error. • session_id: Identificador de la sesión recién creada. Este campo tendrá algún valor únicamente en caso de que no se produzca ningún error. Cuando desee cerra la sesión, debe enviar una petición DELETE con el siguiente formato: • token: Código que identifica una sesión. Al recibir este mensaje, el servidor cerrará la sesión y eliminará el token, de manera que no será válido. Además, enviará la siguiente respuesta: • code: podrá tener los siguientes valores: o 0: Sesión cerrada satisfactoriamente. o 1: El token suministrado no es válido. 3.3.2.2 Contenedores El funcionamiento principal de la API Rest se desarrolla en el recurso container. Responde a los métodos GET, POST, PATCH, PUT y DELETE; y cada uno de ellos desencadenará una acción distinta en el servidor. 3.3.2.2.1 GET Cuando se envía una petición de este tipo al recurso container, el servidor consultará los contenedores asociados a una cuenta, aunque si se especifica el id de un contenedor, se consultará únicamente este contenedor en caso de que exista. Implementación de la solución 48 48 Como se ha mencionado anteriormente, en las peticiones de este tipo, los datos necesarios irán en la URL de la petición como parámetros. En concreto se envían los siguientes: • token: código que identifica a la sesión del usuario. Se consultarán los contenedores que pertenecen a la cuenta asociada a este token. • id: este campo es opcional y permite especifica el contenedor a consultar. Únicamente se responde con la información de este contenedor si pertenece al usuario que realiza la petición. La respuesta del servidor contiene la información de los contenedores encontrados, y sigue el siguiente formato: • code: podrá tener los siguientes valores: o 0: Contenedores consultados correctamente o 1: El token proporcionado no es válido. • description: Mensaje de descripción que varía según el código devuelto. • args: contiene información sobre los contenedores, y se trata de un objeto con un único parámetro llamado “contenedores”, el cual tiene como valor un array con objetos en el siguiente formato: o id: identificador del contenedor o estado: estado del entorno remoto alojado en este contenedor. Puede ser 0 (creando), 1 (activo), 2(modificando) o -1 (que indica que está eliminado y entonces no se recibe este contenedor). o nombre: string asignado por el usuario para identificar al contenedor o cpu: número de procesadores asignados a ese contenedor. o memo_s: Número que representa la memoria RAM en MB o GB solicitada para ese contenedor. El significado de este valor varía en función del parámetro “memo_t”. o memo_t: Formato en el que se ha almacenado la memoria (0 MB, 1GB) o imagen: nombre de la imagen que alberga el contenedor. 3.3.2.2.2 POST Una llamada de este tipo es utilizada por el cliente para solicitar recursos al sistema. El servidor, al procesar esta petición, genera un nuevo contenedor con los recursos especificados en la petición, la cual tendrá el siguiente formato: • token: autentica la sesión que realiza la petición. • cpus: número de núcleos solicitados. Este campo es opcional y su valor por defecto es de 4 núcleos. • mem_s: cantidad de memoria asignada al contenedor en el formato especificado por el campo mem_t. Este campo es opcional y por defecto se asignan 4096MB de RAM. • mem_t: formato en el que se especifica la memoria RAM (0 MB, 1 GB). Este campo es opcional y su valor por defecto es 0. • imagen: identifica la imagen que se desea ejecutar en el contenedor solicitado. Es opcional y su valor por defecto es la imagen “host”. • conexión: identifica el tipo de conexión que se utilizará para comunicarse con la imagen (1 SSH, 0 RDP). Por defecto la conexión será SSH. Cuando ser recibe esta petición, el servidor se comunicará con el maestro de kubernetes a través de su API y solicitará que se arranque un contenedor con las características solicitadas. Como respuesta, se recibe lo siguiente: • code: podrá tener los siguientes valores: o 0: contenedor creado satisfactoriamente. o 1: el token no es válido. 49 o 2: Error al crear el contenedor. Este error realmente engloba bastantes posibles errores. Lo único que detecta realmente la API es que reciba respuesta correcta por parte de la Kube-API. • description: Mensaje de descripción que varía según el código devuelto. El servidor, al recibir la petición, hace uso de una cliente de kubernetes para comunicarse con el nodo maestro mediante la Kube-API. Lo que se solicita es la creación de un deployment al cual se le indica que se desea una única instancia. Con esto se consigue que, si el contenedor cae, kubernetes se encarga de arrancar uno con las mismas características lo antes posible. El deployment contiene todos los datos utilizados por el contenedor, como variables de entorno, credenciales de acceso a las imágenes o los recursos que utilizará. 3.3.2.2.3 PATCH Desencadena un funcionamiento muy similar al de la petición POST, salvo que, en vez de crear un nuevo contenedor, se modifica un contenedor existente. Los parámetros de la petición son exactamente los mismos salvo que se añade el campo id, el cual identifica el contenedor que se desea actualizar. La respuesta por parte del servidor mantiene el mismo formato, y se añade un código de error 3 que significa que el usuario no es propietario del contenedor. En el servidor, al recibir la petición, se modifica el deployment correspondiente al contenedor, de manera que el contenedor actual se elimina y se arranca uno nuevo con las nuevas características. Sería análogo a ejecutar el siguiente comando en el servidor: kubectl apply -f /fichero/deployment.yaml. 3.3.2.2.4 PUT Esta petición es bastante sencilla, y es utilizada por los contenedores para indicar que se encuentran operativos y es posible conectarse a ellos. Únicamente acepta un parámetro, que se envía en la URL y se corresponde con un token que identifica unívocamente al contenedor. La respuesta por parte del servidor es siempre la misma, con el valor “code” a 0 y una descripción indicando que se ha procesado correctamente. En cuanto se recibe esta petición de un contenedor, el cliente, cuando solicite sus contenedores, verá que el entorno remoto está disponible y se puede conectar a él. 3.3.2.2.5 DELETE Este método es utilizado para indicar al servidor que elimine un contenedor previamente creado. El cuerpo de la petición tendrá el siguiente formato: • token: autentica la sesión que realiza la petición. • id: identifica al contenedor que se desea eliminar. Por su parte, la respuesta del servidor sigue el siguiente formato: • code: podrá tener los siguientes valores: o 0: contenedor eliminado correctamente. o 1: el token no es válido. o 2: Error al eliminar el contenedor. • description: Mensaje de descripción que varía según el código devuelto. De nuevo, el servidor, al recibir la petición, se comunica con la Kube-API mediante el cliente kubernetes, y le indica que elimine el deployment. 3.3.2.3 Comando de union Como se mencionó anteriormente en la sección de kubernetes, los nodos worker necesitan una forma de obtener el comando de unión al cluster, y esto se consigue enviando peticiones POST y GET al recurso kubejoin. 3.3.2.3.1 POST Permite al maestro de kubernetes actualizar el comando de unión. El cuerpo de la petición tiene el siguiente formato: Implementación de la solución 56 56 La forma en la que funciona la aplicación del contenedor es, una vez establecida la conexión WebRTC, escucha todos los mensajes recibidos por el canal de comunicación. Cuando se activa un evento de recepción de mensaje, la aplicación procesa el mensaje identificando la acción realizada por el cliente, y mediante la librería nut-js, realiza la misma acción en el entorno remoto. Con todo esto se consiguió realizar una aplicación preliminar que demostraba que se podía realizar una conexión de tipo escritorio remoto con el entrono virtual. Aún faltaban una serie de detalles, puesto que actualmente se usaba un servidor de señalización rudimentario y no se habían desarrollado todas las partes del proyecto. 3.5.1.3 Versión Final Para la versión final del entorno remoto, se mejoraron y añadieron una serie aspectos a la aplicación alojada en su interior. Lo primero es que el código de esta aplicación dejaba mucho que desear, por lo que para la versión final se realizó un proceso de refactorización que lo hiciese legible, fácil de modificar y correctamente comentado. Una vez mejorado todo el código, se añadieron una serie de funcionalidades adicionales. Se configuró la aplicación para que haga uso de la versión final del servidor de señalización, lo que se traduce en implementar el uso del protocolo de comunicación diseñado previamente, responder a los mensajes de keepalive, autenticarse en el propio servidor de señalización, etc. Además, es necesario obtener de alguna forma los datos correspondientes al usuario que se quiere conectar con el entorno, o credenciales del propio entorno. Esto se hace mediante variables de entorno, las cuales son establecidas por la API Rest en el momento de indicar a la Kube-API que arranque el entorno virtual. Además de esta información, se han añadido variables de entorno relativas al resto de servicios utilizados, puesto que, si no se añadiesen, sería necesario reconstruir la imagen cada vez que cambie la forma de acceder al resto de servicios del proyecto. Un ejemplo muy claro es la API Rest, que inicialmente se ejecutaba en AWS y pasó a ejecutarse en el servidor on-premise, lo que hizo que la dirección IP de acceso y su puerto cambiase. Por otro lado, se ha configurado la aplicación para que, una vez esté lista para establecer una conexión, se lo indique al cliente a través de la API Rest. También se ha añadido el código que permite a la aplicación reconectarse con un cliente en caso de que el contenedor se reinicie. Esto es posible gracias a que el servidor de señalización indica si hay algún cliente esperando al establecer una conexión. Por último, se han corregido algunos errores que producían que el uso de memoria aumentase desmesuradamente. 3.5.2 Entorno SSH Otro tipo de conexión que se permite con el entorno remoto es una especie de SSH. Evidentemente, por las razones explicadas en la fase de diseño, no se puede establecer directamente una conexión SSH. La forma en la que funciona es estableciendo una conexión WebRTC con un único canal de datos, y a través de este se envían los comandos que se desean ejecutar en el contenedor, y se devuelve al cliente la respuesta del entorno virtual. A diferencia del entorno anterior, en este no es necesario ningún entorno de escritorio, por lo que no es necesario realizar todo el proceso de crear un escritorio virtual. Es más, al no necesitar capturar un entorno de escritorio, la aplicación no está desarrollada en Electron-js, sino que está desarrollada en node-js. Para que se tenga una experiencia lo más parecida a utilizar ssh, la aplicación del contenedor, al recibir una conexión entrante, inicia un proceso que establece una comunicación ssh consigo mismo. Al recibir algún comando del cliente, lo ejecuta en el proceso y reenvía la respuesta de vuelta al cliente. Esto hace que, en la práctica, el cliente controle un cliente ssh, salvo que lo hace de manera remota a través de WebRTC. Como esta imagen se desarrolló después de la de escritorio remoto, no fue necesario empezar de 0, sino que se partió de la imagen anteriormente descrita. Se eliminó todo el código relacionado con la captura de escritorio y control remoto, y se movió el código a una aplicación en node-js. La forma en la que se muestra esta conexión al cliente se explicará en el apartado de la aplicación de escritorio. 57 3.6 Aplicación de escritorio Por último, se describirá la aplicación de escritorio. Esta es la parte más importante de cara al usuario, puesto que es la forma que tiene de interactuar con todo el sistema. Como se ha mencionado en varias ocasiones a lo largo de este proyecto, se ha desarrollado con Electron-js, que permite desarrollar aplicaciones web como si fuesen aplicaciones nativas del sistema operativo, permitiendo acceso a más bajo nivel mediante el uso de node-js. La aplicación como tal se puede dividir en varias secciones o pantallas: • Zona del cliente. • Zona de conexión con entorno remoto. • Zona del Host. • Zona de autenticación. Se repasarán cada una de ellas indicando cómo se han desarrollado. 3.6.1 Zona del Cliente Es la zona que primero se comenzó a desarrollar y, junto con la del Host, constituye la parte principal de la aplicación. En ella, los usuarios pueden ver y gestionar los entornos remotos que han creado, unirse a ellos, y acceder al resto de funcionalidades de la aplicación. Para la organización de esta sección, se decidió que el usuario debe de poder crear múltiples entornos, y por ello, debe ser capaz de gestionarlos fácilmente. A continuación, se muestra el diseño de esta pantalla: Implementación de la solución 58 58 Imagen 19: Pantalla de Cliente En la imagen anterior (Imagen 19) se pueden diferenciar dos secciones principales. Por un lado, tenemos un encabezado, en el que aparece un switch que permite cambiar entre la zona del cliente y del host. Además, aparece a la derecha un texto que muestra el id de sesión del usuario, y un botón de configuración con la opción de cerrar sesión, aunque la idea es que se añadan más opciones para gestionar la cuenta del usuario. El resto de la sección es un panel que muestra todos los contenedores creados por el usuario. El funcionamiento de esto es bastante sencillo, se consulta a la API todos los contenedores que pertenecen al usuario y se crean dinámicamente unas tarjetas que representan los contenedores. Se puede solicitar que se recarguen los contenedores pulsando el botón de recargar, aunque esta acción se realiza automáticamente de forma periódica. Como se puede apreciar hay dos tipos de tarjeta, una que representa al contenedor y otra que permite solicitar más recursos. Al pulsar el botón para solicitar más recursos (la tarjeta con un símbolo +), se muestra un modal con un formulario, que permite introducir la configuración de los recursos a solicitar (Imagen 20). Los datos que se pueden configurar son el nombre del contenedor, la imagen que se utilizará, la memoria RAM, los núcleos de CPU y el tipo de conexión. Si se pulsa el botón de cancelar, se cancela la creación del contenedor, pero si se pulsa en crear, se realizará una petición a la API Rest para la creación del entorno remoto. Tras unos instantes, se mostrará al usuario un mensaje indicando que su entorno remoto se ha creado correctamente o que se ha producido un error. 59 Imagen 20: Modal creación de contenedor. Tras todo esto, se haya creado o no satisfactoriamente el contenedor, se actualiza la vista de los contenedores. Si se presta atención a la otra tarjeta, ésta representa a un contenedor ya creado. En ella aparece la información más importante del contenedor, y cuenta con un botón que se vuelve activo en caso de que el contenedor sea accesible. En caso de no serlo, se bloquea el botón y muestra el estado actual. En la esquina superior izquierda de la tarjeta, aparece un botón con forma de rueda dentada, que, al pulsarse, muestra un formulario con toda la información del contenedor, y permite realizar varias acciones sobre el mismo. Imagen 21: Modal de edición de contenedor. En el modal de edición se pueden modificar los mismos parámetros que los mostrados en la creación del contenedor, y esto es porque en la práctica, al actualizar un contenedor, se está eliminando el actual y arrancando uno nuevo con las modificaciones aplicadas. Hasta que no se pulse el botón de Guardar, no se aplicará ninguna configuración modificada en el modal, y al pulsarlo, el proceso será muy similar al de creación de contenedor, mostrando un mensaje al terminar la comunicación con el servidor. Adicionalmente este modal permite eliminar el contenedor que se está editando. Al ser pulsado, se muestra un cartel de confirmación, y al aceptarse, la aplicación se comunica con el servidor a través de la API Rest, y elimina Implementación de la solución 60 60 el contenedor actual. De manera similar a las operaciones anteriores, se muestra un mensaje al recibir la respuesta del servidor y se recargan automáticamente la vista de contenedores. 3.6.2 Zona de conexión con entorno remoto Cuando se pulsa el botón de conexión en la tarjeta del contenedor, se lleva al usuario a esta nueva zona para que pueda interactuar con el entorno remoto. Esta sección variará según se trate de una conexión de tipo escritorio remoto o de tipo consola de comandos, pero ambas muestran una ventana similar. Imagen 22: Ventana de conexión con entorno remoto. La mayor parte de la ventana está ocupada por un espacio vacío, en el que se muestra un texto de ‘Cargando…’. Cuando se entra en esta pantalla, se comienza a establecer la comunicación con el entorno remoto, y en el momento que esta se establezca, se mostrará la terminal de comandos o el escritorio del entorno remoto, en función del tipo de conexión. Para la conexión de tipo escritorio remoto, se muestra en el espacio vacío el vídeo recibido por el canal de vídeo de la conexión WebRTC. Cuando el usuario mueve el ratón por encima de este vídeo, se capturan los eventos generados en javascript y se envía la posición del ratón dentro de la pantalla a través del canal de comunicación. La posición del ratón está normalizada, de manera que es independiente del tamaño de la pantalla origen y destino. En el entorno remoto, se captura estas coordenadas, se transforman en coordenadas del espacio de la pantalla virtual y se mueve el ratón hasta esa posición. Adicionalmente, si se pulsa cualquier tecla, o se realiza cualquier acción con el ratón, la aplicación de escritorio la captura (siempre que esté en primer plano) y envía el evento al entorno remoto. Desafortunadamente, el entorno remoto no cuenta con hardware dedicado a la codificación de vídeo, por lo que se hace notable la latencia (de unas décimas de segundo) cuando se mueve el ratón por la pantalla, aunque se ha probado con diversos formatos de compresión para hallar el que ofreciese mejor latencia con mejor calidad de imagen, primando la latencia sobre la calidad de imagen. Pasando a la conexión de tipo consola de comandos, la forma de operar es similar. La aplicación se conecta con el entorno remoto y, cuando se establece la conexión, se muestra una terminal de comandos con la que el usuario puede interactuar como lo haría con cualquier otra terminal. Esta terminal se ha implementado utilizando la 61 librería de javascript xterm-js [21], la cual cuenta con una buena cantidad de documentación tanto oficial como de terceros. Imagen 23: Ventana de conexión entrono remoto terminal. En ambos modos de conexión, a la derecha aparece una pestaña con una flecha que al ser pulsada muestra un menú con varios botones. En este momento sólo cuenta con dos botones, aunque la idea es que se añadan en esta sección todos los botones relacionados con las funcionalidades que se vayan desarrollando. Los dos botones que hay actualmente son el de pantalla completa y el de desconexión. El de pantalla completa, como su nombre indica, muestra la aplicación en modo inmersivo y ocupando toda la pantalla, para que sea más cómodo trabajar en el entorno remoto. Por otro lado, el botón de desconexión libera la conexión del servidor de señalización, la conexión WebRTC y devuelve al usuario a la zona de clientes. Adicionalmente, la aplicación escucha continuamente a mensajes de keepalive del servidor de señalización, ante los cuales responde para indicar que sigue conectada. 3.6.3 Zona del host Otra sección bastante importante de la aplicación es la zona del host. En esta, el usuario puede compartir sus recursos. Si bien esta sección tiene menos contenido con el que puede interactuar el usuario, tiene algo más de lógica detrás. A continuación, se muestra una captura de esta sección (Imagen 24). Implementación de la solución 62 62 Imagen 24: Pantalla de Host En la pantalla, además de la cabecera que es común para las pantallas de cliente y host, hay un único botón, el cual permite comenzar a compartir los recursos. Cuando es pulsado, se muestra al usuario una ventana con la configuración de la máquina virtual que posteriormente se abrirá, pero antes de ello, comprueba que el software necesario se encuentre instalado en el equipo, es decir, comprueba que Vagrant, el plugin de vbguest y VirtualBox se encuentren instalados. En caso de no encontrarse este software instalado en el equipo, la aplicación intentará instalarlo automáticamente. Imagen 25: Modal de edición de máquina virtual En esta ventana se puede configurar la ubicación donde se almacenarán los datos de la máquina virtual, los cpus 63 y la RAM que se quiere asignar a la misma. Cuando se selecciona el campo de la ruta, se abre una ventana nativa del sistema operativo para poder seleccionar la ubicación en el equipo de manera más sencilla. Esto se puede hacer gracias a que en Electron-js se tiene acceso a node-js, y con ello, se pueden acceder a un nivel más profundo del sistema operativo. Además, se comprueba que el directorio se encuentre vacío, y en caso de no estarlo, se muestra un mensaje al usuario y prohíbe continuar la ejecución para así evitar sobrescribir datos del usuario. Una vez configurado todo, si se pulsa el botón de crear, se procede a arrancar la máquina virtual. Mientras que el usuario en este momento sólo verá una ventana de error o éxito al finalizar todo el proceso, la aplicación internamente realiza bastantes tareas. Lo primero que hace la aplicación es cargar los ficheros necesarios para poder arrancar la máquina virtual, es decir, el fichero vagrantfile, los scripts a los que accederá la máquina virtual y archivos yaml. Entonces consulta el nombre que tendrá la máquina virtual mediante una llamada a la API Rest, y configura adecuadamente el fichero vagrantfile con el nombre de la máquina junto con la configuración introducida por el usuario (RAM y CPUS). La aplicación continúa comprobando la dirección IP que se deberá asignar al nodo worker y configurará los ficheros necesarios con la nueva dirección. Seguidamente, se consulta el comando de unión al cluster y se modifica el script que ejecutará la máquina virtual al arrancar, de manera que el comando se encuentre actualizado. Una vez esté todo configurado, se procede a arrancar la máquina virtual, creando un nuevo proceso que ejecuta el comando vagrant up en el directorio correspondiente. Este comando tardará un tiempo en ejecutarse, y es posible que falle, de manera que cuando termine su ejecución se comprobará si se ha ejecutado correctamente o no y se informa al usuario en consecuencia. Hay que resaltar que el proceso de obtener la dirección IP y el comando de unión se realiza cada vez que se arranca la máquina virtual, puesto que así se garantiza que se tenga la información más actualizada. A partir de este momento, hay una nueva máquina virtual arrancada y funcionando. Como consecuencia, al lado del botón de comenzar a compartir ha aparecido un botón de configuración. Este botón, al ser pulsado, muestra un formulario (Imagen 25) que permite modificar las características de la máquina virtual, e incluso eliminarla. Si se pulsa el botón de guardar, se actualizan los ficheros en el directorio donde se encuentra la máquina virtual, de manera que, en el próximo arranque, tenga la configuración aplicada. Por el contrario, si se pulsa el botón de eliminar, se procederá a ejecutar el comando vagrant destroy -f de manera que la máquina virtual se elimina permanentemente, y se eliminan todos los ficheros que se hayan creado en el sistema. Si se desea arrancar de nuevo una máquina virtual, es necesario pasar por todo el proceso de creación desde el principio. Por último, si se desean dejar de compartir recursos, se puede pulsar el botón de “Dejar de compartir” que sustituye al botón de “Comenzar a compartir” cuando la máquina virtual se encuentra en marcha. Cuando se pulsa este botón, la aplicación elimina la máquina virtual en ejecución realiza el proceso inverso al comando de unión. Para ello, elimina la dirección IP asignada al nodo worker y elimina el nodo del cluster mediante una llamada a la API Rest. En un inicio, al dejar de compartir los recursos, se detenía la máquina virtual y se deshacía toda la configuración, pero tras unas pruebas resultó ser más efectivo directamente eliminar la máquina virtual. Para no tener que hacer el proceso de configuración cada vez que se arrancase la máquina virtual, se almacena toda la configuración del usuario en la memoria del sistema y al crearse la nueva máquina virtual, se crea en base a esta configuración. Como se puede entender, la máquina virtual se puede controlar de manera limitada por la aplicación, y se ejecuta independientemente a ésta, lo que quiere decir que, si la aplicación se cierra, la máquina virtual seguiría ejecutándose en segundo plano. Pese a que lo correcto sería que el usuario cerrase la máquina virtual antes de cerrar la aplicación, y que lo hiciese a través de la misma, es algo ingenuo pensar que el usuario final se comportará así. Por ello, es necesario tomar algunas medidas para que no se entre en un estado no planeado. Cuando se inicia la aplicación, se comprueba automáticamente si existe la máquina virtual en el equipo, y en caso afirmativo, se comprueba su estado. Esto se hace gracias a que, en el momento de la creación, se almacenó en la ruta de instalación de la aplicación un fichero con información relativa a la máquina virtual, en concreto su ubicación. Al arrancar la aplicación, se consulta este fichero, y en caso de existir, se ejecuta vagrant status en la ruta donde se encuentran los ficheros de configuración de la máquina virtual, y con ello se obtiene su estado. Si la máquina virtual está encendida, se actualiza la interfaz de usuario para indicarlo, pero en caso de encontrarse apagada, como medida de seguridad, se llama a la API para desvincular la dirección IP de esta máquina. Con Implementación de la solución 64 64 ello, se garantiza que no haya ninguna máquina acaparando una dirección IP si no la está usando. Además, se elimina el nodo worker correspondiente del cluster de kubernetes mediante una llamada a la API Rest, de forma que se evita la asignación de contenedores a nodos workers que no están en ejecución. Como medida adicional, al arrancar la máquina virtual, previamente se elimina la que pudiese estar ejecutándose mediante el comando vagrant destroy -f, de manera que siempre se arranca del mismo estado inicial. Pese a todas estas medias de seguridad, Vagrant no termina de funcionar del todo bien, y hay veces que se no consigue arrancar correctamente la máquina virtual. Quizás una forma de arreglar esto es no depender de Vagrant, sino comunicarse directamente con la aplicación virtualizadora, aunque esto llevaría bastante tiempo de investigación y sería una posible mejora. 3.6.4 Zona de autenticación Como última zona de la aplicación, se describe, irónicamente, la primera ventana que ve el usuario al arrancar la aplicación. Se trata de la ventana de autenticación y no tiene una gran complejidad. Se comporta como cualquier otra ventana de autenticación de cualquier otra aplicación, enviando peticiones a la API Rest en base a la información aportada por el usuario. Imagen 26: Pantalla de inicio de sesión Como algo a destacar, se puede mencionar que se ha añadido la opción de mantener la sesión iniciada de manera persistente. Esto se consigue almacenando el token de sesión en un fichero del sistema operativo, de manera que cuando se abre la aplicación, se comprueba si este fichero existe y, en caso de existir, se comprueba que el token siga siendo válido. Si todo sale correctamente se permite ingresar en la aplicación al usuario, pero se le prohíbe en caso contrario. 3.6.5 Interfaz de usuario En esta sección se va a comentar el proceso de diseño de la interfaz de usuario, dado que, pese a parecer una tarea sencilla, lleva más tiempo del que parece. 65 La estética que estaba buscando era una similar a varios programas que había utilizado anteriormente, es decir, una estética minimalista con colores planos y de tonos grisáceos. Además, quería que todo estuviese accesible a unos pocos clicks, sin tener que navegar menús infinitos. Para el diseño, he tomado inspiración principalmente de la aplicación Parsec, y pese a que he intentado diferenciarme de ella, ha acabado pareciéndose más de lo que pretendía. Se ha optado por una paleta de colores en diferentes tonos de gris, y como color primario se ha usado el púrpura. Además, se ha intentado jugar con sombras y variaciones en la tonalidad para dar la sensación de profundidad. El uso del color primario se ha decidido limitar a botones específicos para que resalten más sobre el fondo gris. Imagen 27: Estado botones A lo largo de la aplicación, se han usado diferentes tipos de animaciones, tanto en los botones como en las ventanas para dar dinamismo a la misma. Para desarrollar estas animaciones, se ha intentado usar principalmente transiciones de estados en el css, y en los casos en los que se ha utilizado javascript, se ha limitado a añadir o eliminar clases css a los objetos del DOM. En este documento no se pueden mostrar, pero se han realizado animaciones para la transición entre algunas pantallas, como los distintos modos en el inicio de sesión o para pasar del modo cliente al modo host. Otras animaciones son efectos al pasar el ratón encima de botones, o al mostrar y cerrar modales. Además, decidí renderizar un video en Cinema4D y ponerlo como fondo en la pantalla de inicio de sesión. De esta forma se generaría un gran impacto visual en la primera ventana que ve el usuario final. Adicionalmente, se diseñó un logotipo en Figma y se utilizó como icono de la aplicación. Imagen 28: Logotipo SharePC Si bien no era necesario dedicar tanto esfuerzo en la aplicación, me apetecía demostrar mis capacidades de diseño y hacer algo que sea realmente agradable de utilizar. Tengo la sensación de que, en el mundo del software, y sobre todo en el mundo del desarrollo open-source, se tiende mucho a dejar de lado la parte Implementación de la solución 72 72 al entorno remoto, y tras recibir la respuesta, actualiza la entrada en la base de datos indicando que el contenedor se ha eliminado. El cluster de kubernetes, al recibir la orden de eliminar el deployment, indica a los workers que eliminen todos los contenedores asociados a éste. 73 4 PLAN DE PRUEBAS Como penúltima sección del documento, se procederá a comentar las pruebas realizadas a lo largo del proyecto. A medida que se han ido desarrollando las distintas partes del proyecto, se han ido realizando una serie de pruebas para asegurar que todo funcionase correctamente. Si bien lo ideal sería tener pruebas automatizadas de manera que se pueda probar por cada cambio, este proyecto únicamente trata la parte inicial de desarrollo, por lo que no tiene mucho sentido invertir tiempo en crear una gran cantidad de test que solo se van a utilizar una vez. Se va a realizar un recorrido por todas las pruebas realizadas en cada uno de los componentes del proyecto. 4.1 Entorno Kubernetes inicial El proyecto se inició comprobando que se pudiesen crear clusters de kubernetes en los que los nodos worker se encontrasen en una subred distinta y detrás de NAT, sin necesidad de abrir ningún puerto en el router del worker. Para probarlo, se arrancó un nodo maestro sencillo y se creó otro worker en un ordenador distinto. Inicialmente el nodo maestro se encontraba en la misma red que el worker, y se probó a crear un deployment y se revisó que se creaban los contenedores correspondientes en el nodo worker. Entonces se probó a cambiar el worker a una subred distinta, conectando el equipo donde se ejecutaba a una conexión móvil, y se procedió a repetir el proceso. Cabe destacar que, pese a que los test fuesen correctos, las conclusiones que se obtuvieron fueron erróneas. Al realizar las pruebas inicialmente se revisó que los contenedores se desplegasen en los workers, pero esto se hizo únicamente desde el maestro, y no se probó a entrar en el contenedor para ver que se ejecutaba correctamente. Esto resultó en que se considerase que el plugin de red utilizado inicialmente, calico, funcionara correctamente, cuando la realidad es que no lo hacía, lo que provocó que se perdiese mucho tiempo más adelante, como se explicó en la sección de desarrollo. 4.2 Aplicación preliminar WebRTC Una vez se comprobó que un nodo worker podía unirse al cluster, se comenzó a desarrollar una aplicación preliminar para asegurar que se podía establecer una conexión WebRTC con el entorno remoto. Primero se probó la aplicación en el mismo ordenador en el que se estaba desarrollando. Una vez se ha asegurado que se transmite correctamente el vídeo, se probó a ejecutar la aplicación en un contenedor de Docker ejecutándose en una máquina virtual. Gracias a esta prueba se descubrió que hacía falta configurar el contenedor para que crease un escritorio virtual en su interior. Conforme se iban aplicando cambios, se fue comprobando que funcionase correctamente, hasta que finalmente se consiguió establecer la comunicación correctamente. Para probar un escenario más real, se probó a ejecutar el contenedor en un ordenador distinto al que ejecutaba el cliente y se configuró este ordenador para que se encontrara una red distinta. Con ello se descubrió que, en este escenario, era necesario un servidor TURN, al no poderse establecer una conexión directa. Aprovechando este escenario, se hicieron pruebas sobre el formato de codificación y de vídeo utilizado para transmitir el escritorio en tiempo real, encontrando el formato que mejor funcionaba. 4.3 Pruebas de la aplicación de escritorio Una vez se comprobó que todo el diseño planteado podría funcionar, se comenzó a desarrollar la aplicación de escritorio. Las pruebas realizadas consistían en hacer un recorrido por la funcionalidad que se estaba desarrollando y probar todas las posibles casuísticas. Por ejemplo, se probaba a cancelar una operación, a realizarla varias veces seguidas, se probaba el comportamiento ante llamadas a la API erróneas, etc. Con ello, se Plan de pruebas 74 74 descubrió una gran cantidad de situaciones en las que la aplicación quedaba en un estado de bloqueo y no permitía al usuario interactuar con ella a menos que la reiniciara. Como la aplicación de completó de desarrollar antes de contar con una API Rest o un servidor de señalización, se utilizaban recursos de prueba, y a medida que se iban desarrollando el resto de las componentes del proyecto, se iba probando la aplicación, y asegurando que el comportamiento fuese el esperado. Como cada componente de la aplicación afectaba únicamente a una parte de la aplicación, se hacían pruebas en esa sección. Las pruebas que se realizaban eran: • Autenticación. Todo el proceso de inicio de sesión, cerrado de sesión y creación de cuenta. • Contenedores. Probar a crear un contenedor, editarlo, conectarse con él, desconectarse y eliminarlo. • Host. Comenzar a compartir los recursos, modificar la máquina virtual, eliminarla, probar a ejecutar la aplicación sin Vagrant o VirtualBox instalado. En definitiva, la aplicación de escritorio se probó haciendo un recorrido por la misma. Este tipo de pruebas, además de comprobar que la aplicación funcionaba bien, permiten asegurar que los demás recursos desarrollados también funcionan correctamente. 4.4 API Rest Para la API Rest se hicieron pruebas utilizando la aplicación Postman. En ella estaban almacenadas todas las posibles interacciones con la API, por lo que era cuestión de ir probando cada uno de los métodos para cada uno de los recursos, y asegurar que los resultados fuesen los esperados. Una vez comprobado que la API funcionaba correctamente, se probó a implementarla con la aplicación de escritorio, de manera que se probaba en escenarios nuevos. Adicionalmente, como la aplicación no funcionaba perfectamente desde el principio ni realizaba las llamadas como se esperaban, se pudo comprobar el funcionamiento de la API ante llamadas erróneas o campos incompletos. Esto ayudó desarrollar una API Rest con mayor robustez y menos fallas de seguridad. 4.5 Servidor de señalización El servidor de señalización comenzó siendo algo simple para poder probar la conexión WebRTC, pero cuando se comprobó que la conexión era posible, se desarrolló una versión con más funcionalidades. La manera en la que se probó fue conectando clientes de prueba y hacer que establecieran una conexión. Se probó a que esos clientes no enviasen los credenciales correctos o que solicitasen la comunicación con algún contenedor que no fuese suyo. Continuamente se repasaban los logs tanto de los clientes como del servidor de señalización para comprobar que funcionaba correctamente. Además, cuando estuvieron preparados el cluster de kubernetes y la aplicación de escritorio, se probó a arrancar contenedores mediante kubernetes y a comprobar que la comunicación se produjese satisfactoriamente cuando debía de serlo, y de forma errónea cuando no debía de serlo. Al probarlo con otras partes en desarrollo, se daban casuísticas más extrañas y se podía probar el servidor en más escenarios. 4.6 Pruebas de integración con todos los componentes Como se mencionó en las pruebas de la aplicación, los distintos componentes del proyecto se iban uniendo entre sí conforme se iban terminando de programar. Inicialmente la mayoría de las componentes eran de prueba con una funcionalidad limitada, pero conforme se iban desarrollando, se iban sustituyendo por las versiones finales. De esta forma todo el proyecto iba evolucionando en conjunto. 75 Al probarse los componentes finales con otras partes que no estaban del todo terminadas, se generaban estados que no se habían contemplado y hacía que las pruebas fuesen más variadas. Cuando se tuvo la aplicación desarrollada, la mayoría de las pruebas que se realizaban consistían en hacer un recorrido por la aplicación, comprobando que no había dejado de funcionar nada. Cada vez que se realizaban pruebas, se mostraba la consola tanto de la API Rest, como de la aplicación y del maestro kubernetes. A veces incluso la de los workers, de manera que se podía ver como afectaba una acción del usuario al resto de componentes del sistema. Una vez se habían desarrollado todos los componentes, se hizo un recorrido completo por la aplicación de escritorio. Esto se hizo en varios escenarios de red y con múltiples dispositivos, comprobando que el comportamiento era el adecuado y que se cumplían todos los requerimientos planteados en la fase de diseño. Conclusiones y líneas de mejora 76 76 5 CONCLUSIONES Y LÍNEAS DE MEJORA Como se mencionó en la introducción, el objetivo de este proyecto era conseguir realizar una aplicación que permita utilizar a los clientes el hardware de los hosts, para poder trabajar en remoto en él. La idea inicial era que un usuario cualquiera pudiese utilizar el equipo de otro usuario de manera remota y que pudiese utilizarlo para tareas intensivas como jugar a videojuegos o renderizar vídeo. Pero al poco de plantear el proyecto se decidió simplificar el alcance del mismo, puesto que se disponía de apenas 4 meses de los cuales uno de ellos se dedicaría a escribir el documento y posiblemente otro tuviese una disponibilidad limitada. Se decidió por llevar a cabo una solución más simplificada, sin tener en cuenta aspectos de rendimiento o de latencia, que sentase las bases de la que podría ser una versión final. El proyecto que se ha acabado llevando a cabo permite a los hosts compartir sus recursos hardware, como RAM o CPU, y a los clientes hacer uso de estos recursos estableciendo una conexión con entornos remotos ejecutados en los equipos de los hosts. Ni los clientes ni los hosts tienen necesidad de tener conocimientos previos sobre informática o telecomunicaciones, puesto que disponen de una aplicación que hace todo el trabajo por ellos. Además, ningún usuario puede acceder a los datos personales del resto de usuarios de la aplicación, garantizando la seguridad e integridad de los mismos. Entre las características principales de la solución desarrollada se encuentra: • Se ha desplegado un cluster de kubernetes en el que los nodos worker se encuentran en redes completamente distintas y en escenario de red desconocidos, y se ha automatizado el despliegue de contenedores en los hosts. • Se han creado contenedores que albergan entornos de escritorio y que permiten al cliente hacer uso del hardware ofrecido por los hosts. En su interior se ejecuta una pequeña aplicación desarrollada que permite a los clientes conectarse a su entorno remoto en forma de escritorio remoto o de terminal de comandos, sin importar las condiciones de red del cliente o el contenedor. • Se ha desarrollado satisfactoriamente un servidor de señalización en node-js que permite la conexión entre el cliente y su entorno remoto. • Se ha desarrollado y desplegado una API Rest haciendo uso de Python, Flask, Gunicorn, Gninx y un servidor de base de datos PSQL. • Se ha desarrollado una aplicación de escritorio haciendo uso de Electron-js que permite a los Host compartir sus recursos mediante la creación de una máquina virtual, y a los clientes solicitar recursos y establecer una conexión remota con sus entornos remotos. • Se han desplegado en un servidor on-premise todos los servicios utilizados. Todos los servicios se ejecutan en contenedores, a excepción del maestro de kubernetes y la base de datos. • Se han protegido todos los servicios desplegados, de manera que únicamente clientes de la aplicación puedan hacer uso de ellos a través de la misma. Hay que recalcar que la solución está muy lejos de ser perfecta y presenta gran cantidad de limitaciones. • La más evidente es que los clientes solo pueden utilizar el CPU y la RAM remotos, pero no pueden utilizar por ejemplo la tarjeta gráfica, lo que limita la potencia de cómputo utilizable. • Los contenedores pueden tardar bastante tiempo en arrancarse, hasta el punto de tardar más de 5 minutos, lo cual empeora la experiencia del usuario al usar la aplicación. • La implementación del despliegue de máquinas virtuales con Vagrant deja mucho que desear, puesto que falla en multitud de ocasiones. • La conexión entre cliente y host, pese a ser satisfactoria en la mayoría de los casos, presenta una latencia notable, y la calidad del vídeo recibido deja mucho que desear. En general estoy satisfecho con el resultado obtenido, puesto que, pese a las limitaciones que presenta la 77 solución, se han conseguido cumplir los objetivos planteados al inicio del proyecto. Durante el transcurso de todo el proyecto, he conseguido aprender sobre una gran cantidad de tecnologías. He aprendido mucho sobre kubernetes y contenedores, aprendiendo realmente cómo funcionan internamente y cómo se pueden configurar. Como consecuencia de crear tantos contenedores, he ampliado mis conocimientos sobre Linux. He aprendido otro framework para la creación de aplicaciones de escritorio, lo que a su vez que ha permitido profundizar, irónicamente, sobre el desarrollo de aplicaciones web (html, css y javascript). Personalmente era algo reticente a utilizar javascript, pero mi opinión acerca de este lenguaje ha cambiado completamente, hasta el punto que se ha convertido en uno de mis lenguajes preferidos. No solo lo he utilizado en el front-end para el desarrollo de la aplicación de escritorio, sino que lo he utilizado en el back-end para el desarrollo del servidor de señalización. Adicionalmente, he ampliado mis conocimientos sobre APIs Rest y he desplegado una en Flask, Gunicorn y Gninx. 5.1 Líneas de mejora El proyecto ha cumplido su propósito de servir de base para demostrar que una aplicación de este tipo es posible, y por ello existen una gran cantidad de líneas de progreso, tanto en forma de corrección de los errores presentes como en adición de funcionalidades. • Latencia. Los entornos remotos presentan una notable latencia, por lo que una posible línea de mejora sería mejorar esta conexión. Existen multitud de formas de reducir la latencia, pero se destacan dos. Por un lado, los contenedores remotos no cuentan con hardware para la codificación de vídeo, por lo que pueden estar tardando demasiado en codificar el vídeo obtenido de grabar el escritorio, y podría ser buena idea conseguid hacer uso de la tarjeta gráfica en el entorno remoto. Por otro lado, la aplicación en el interior del contenedor está desarrollada en electron-js, y su rendimiento deja mucho que desear, por ello se podría optar por realizar una implementación más eficiente de esta codificación y transmisión de vídeo mediante WebRTC. • Eliminar el uso de Vagrant. El arranque de las máquinas virtuales está implementado en la aplicación de escritorio haciendo uso de Vagrant. Pese a que es una opción válida, Vagrant suele fallar en gran cantidad de ocasiones, lo que hace que en la práctica no sea viable. Una buena alternativa podría ser comunicarse directamente con la aplicación virtualizadora, como VirtualBox. Esto conlleva modificar la forma en la que se configura la máquina virtual y, en general, toda la lógica de creación de máquinas virtuales. • Eliminar el uso de kubernetes. En el proyecto, los entornos remotos se ejecutan en contenedores desplegados mediante kubernetes, y aunque esto no tiene nada de malo, sería interesante probar a que los entornos remotos se ejecuten en contenedores. Para ello sería necesario implementar toda la orquestación realizada por kubernetes, lo cual supondría un gran trabajo, aunque podría conllevar a una mejora de rendimiento en general. • Almacenamiento remoto. En la solución actual, los datos del entorno remoto del cliente se almacenan en el host, por lo que, si el cliente pierde la conexión con su entorno, no puede acceder a sus datos. Esto podría solucionarse haciendo uso de carpetas compartidas, ya sea alojadas en un servidor central o en las máquinas de los clientes. • Imágenes de contenedor personalizadas. En la solución actual, los clientes deben elegir entre una serie de contenedores disponibles, pero una de las ideas iniciales al diseñar el proyecto, era que el cliente pudiese utilizar su propia imagen en el entorno remoto, aunque no se pudo implementar por falta de tiempo. Para esto existen varias alternativas, una opción podría ser que el cliente subiese su propio dockerfile y la aplicación se encargase de construir la imagen de contenedor (ya sea en el ordenador del cliente o en otro lugar). Otra opción sería que el usuario construyese su propia imagen y la subiese a algún repositorio de imágenes, y fuese esta imagen la que se utilizase en vez de las imágenes ya existentes. En ambos casos sería necesario modificar la imagen resultante para que incluyese el código necesario para la conexión WebRTC. Estas son solo algunos de los posibles cambios que podrían realizarse sobre el proyecto, aunque existen muchísimas modificaciones posibles. Anexo I: Guía de despliegue del servidor 78 78 ANEXO I: GUÍA DE DESPLIEGUE DEL SERVIDOR En este anexo se describirá el proceso a seguir si se desea desplegar la solución planteada a lo largo del documento. Como se ha comentado a lo largo del documento, este proyecto consta de muchas partes distintas, y se explicará el proceso para desplegar cada una de ellas. En concreto, se mostrará el proceso de despliegue de lo siguiente: • Nodo maestro de kubernetes. • API Rest. • Servidor de Señalización. • Servidor Coturn. • Servidor de base de datos PSQL. • Imágenes de Entornos Remotos. • Imagen de nodo worker. Todos los ficheros mostrados a continuación son accesibles públicamente a través del repositorio de GitHub del proyecto [22]. 6.1 Maestro Kubernetes Para la instalación del maestro de kubernetes, basta con ejecutar el script master.sh en el servidor donde se desee ejecutar el maestro. En este script es necesario especificar la versión del sistema operativo y la versión de kubernetes, junto con los rangos de direcciones IP que se quieran designar, aunque la configuración por defecto debería de valer. Una vez configurado y ejecutado este script, si se ejecuta el siguiente comando, se deben mostrar una serie de pods, y tras un periodo, todos deberían pasar a running: $ sudo kubectl get po -o wide –all-namespaces En caso de que algunos pods no se llegasen a arrancar correctamente, será necesario eliminar el pod concreto mediante el siguiente comando. $ sudo kubectl delete po -n <namespace-del-pod> <nombre-del-pod> Esto se produce porque el Daemon no le ha dado tiempo a arrancarse y por ello algunos pods fallan. Al reiniciarlos una vez el Daemon esté arrancado, los pods arrancan sin problemas. Otro posible error que se puede encontrar es que, cuando un worker se una al cluster, tenga problemas a la hora de arrancar contenedores ordenados por el maestro. Esto puede ser posible a causa de que se está utilizando la dirección IP privada del maestro, en vez de la dirección accesible públicamente. Para solucionar esto, se debe configurar el Daemon set de Flannel, mediante el siguiente comando. 79 $ sudo kubectl edit ds kube-flannel-ds -n kube-system En el archivo que se abra, será necesario añadir dos variables de entorno con la dirección IP pública del maestro de kubernetes y el puerto correspondiente. Una vez configurado esto, quizás sea necesario reiniciar los pods correspondientes a Flannel, como se explicó en el comando anterior. Entonces, el maestro kubernetes debería de funcionar sin ningún problema. En caso de encontrar errores, sería recomendable comprobar que los rangos de direcciones IP establecidos no interfieren los unos con los otros. A continuación, es necesario configurar los credenciales del repositorio de imágenes Docker, para que los workers puedan descargar las imágenes indicadas por el maestro. En este caso se ha utilizado el repositorio Docker Hub, aunque se puede utilizar cualquier otro. Para generar las credenciales se ejecuta el siguiente comando en el servidor: $ kubectl create secret docker-registry regcred --docker- server=https://index.docker.io/v1/ --docker-username=<your-name> --docker- password=<your-pword> --docker-email=<your-email> En el anterior comando es necesario especificar una cuenta de correo electrónico, nombre de usuario y contraseña. El valor del parámetro –docker-server variará según el repositorio de imágenes utilizado, aunque el especificado se corresponde con Docker Hub. 6.2 API Rest La API Rest se encuentra completamente desarrollada. Para desplegarla en el servidor es necesario primero construir la imagen de Docker y después ejecutarla. Para construirla se utiliza el siguiente comando en el directorio donde se encuentren los archivos relativos de la API. $ docker built -t <nombre-imagen> . Esto generará una imagen. Para arrancarla, se ejecuta el siguiente comando, especificando una serie de parámetros. • HOST_API. Dirección IP o dominio al que escuchará la API. • PORT_API. Puerto al que escucha la API. Anexo I: Guía de despliegue del servidor 80 80 • HOST_WS. Dirección IP o dominio al que escucha el servidor de señalización. • PORT_WS. Puerto al que escucha el servidor de señalización. • HOST_TURN. Dirección IP o dominio al que escucha el servidor TURN/STUN. • PORT_TURN. Puerto al que escucha el servidor TURN/STUN. • USER_TURN. Usuario para utilizar el servidor TURN/STUN. • PASS_TURN. Contraseña para utilizar el servidor TURN/STUN. • <puerto-escucha-api>. Puerto local al que escuchará la API. $ docker run -it -d -p <puerto-escucha-api>:80 -e HOST_API = <> -e PORT_API=<> -e HOST_WS=<> -e PORT_WS=<> -e HOST_TURN=<> -e PORT_TURN=<> -e USER_TURN=<> -e PASS_TURN=<> --dns 8.8.8.8 --dns 8.8.4.4 --name share_api <nombre-imagen> Adicionalmente, es necesario copiar un fichero de configuración de kubernetes. Este fichero se encuentra en “/root/.kube/config”, y se copiará en la carpeta donde se encuentren todos los archivos de la API. Este fichero será utilizado por la API para comunicarse con el maestro de kubernetes mediante la Kube-API. 6.3 Servidor de señalización Similar a la API Rest, para desplegar este servicio basta con crear la imagen de Docker y arrancarla. Para arrancar el contenedor, se utilizará el siguiente comando: $ docker run -it -d -p <PORT_WS>:<PORT_WS_PRIV>-e HOST_API=<> -e PORT_API=<> -e PATH_API=<> -e PORT_WS=<> --dns 8.8.8.8 --dns 8.8.4.4 --name signaling signaling En el anterior comando será necesario configurar una serie de parámetros. • HOST_API. Dirección IP o dominio al que escuchará la API. • PORT_API. Puerto al que escucha la API. • PATH_API. Recurso al que se debe llamar a la API para autenticar las conexiones. • PORT_WS. Puerto donde escucha el servidor de señalización. • PORT_WS_PRIV. Puerto interno del contenedor donde escucha el servidor de señalización. 6.4 Servidor Coturn Para configurar este servicio, es necesario modificar el fichero de configuración turnserver.conf y especificar el campo listening-ip con la dirección IP privada del servidor, y external-ip con la dirección pública y privada del servidor con el formato <public_addr>/<priv_addr>. Una vez configurado, se puede ejecutar el contenedor con el siguiente comando: $ docker run -d --network=host -v $(pwd)/turnserver.confg:/etc/coturn/turnserver.conf --name coturn coturn/coturn El comando deberá ejecutarse en la ubicación donde se encuentre el archivo de configuración. 81 6.5 Servidor de Base de Datos PSQL Para instalar PostgreSQL basta con ejecutar el siguiente comando: $ sudo apt-get install postgresql Una vez instalado, es necesario realizar alguna configuración adicional. Esta configuración variará según el usuario y existe una gran cantidad de documentación en línea sobre cómo realizarla, como por ejemplo [23]. Cuando se haya realizado la configuración necesaria, es necesario especificar en la API los datos sobre la Base de datos. Para ello, es necesario modificar el archivo database.py y especificar datos como la dirección de conexión o el usuario. Para la dirección del host, se recomienda utilizar la dirección IP más directa a la base de datos, puesto que, si no, la API puede demorarse demasiado en realizar las consultas. En el caso de este proyecto, al encontrarse la API en un contenedor Docker y la base de datos en el sistema operativo host, la dirección especificada es la dirección IP que utiliza el contenedor como Gateway para comunicarse con el resto del servidor, en nuestro caso, la dirección “172.17.0.1”. Evidentemente, al realizar estos ajustes en la API, será de nuevo necesario construir la imagen del contenedor y ejecutarla. 6.6 Imágenes entornos remotos Para que los nodos worker puedan acceder a las imágenes correspondientes de los contenedores, es necesario crear estas imágenes y subirlas a algún repositorio de imágenes. En el caso de este proyecto se utilizará Docker Hub. El proceso para compilar una imagen es sencillo y similar al de las imágenes anteriores. Una vez se ha construido la imagen, es necesaria añadirle un tag y subirla. El repositorio utilizado y el nombre de la imagen se especifican en la API, y en el proyecto actual se seleccionan bajo el nombre de denis31599/tfm_test. Para identificar imágenes individuales se hace uso del tag mediante el siguiente formato: <nombre>_<tipo> siendo el nombre un identificador para la imagen y el tipo indica el tipo de conexión que acepta (ssh en caso de terminal o nada en caso de escritorio remoto). Para subir la imagen es necesario primero añadir el tag y después subirla, aunque previamente es necesario haberse autenticado en Docker. Esto se realiza con los siguientes comandos: //login en Docker $ docker login --username foo --password-stdin //Tag de la imagen $ docker tag <nombre_imagen> <usuario>/<imagen>:<tag> //Subir la imagen $ docker push <usuario>/<imagen>:<tag> Pese a que se encuentran los dockerfile de las imágenes de los entornos remotos, en el caso de la imagen de escritorio remoto, la construcción falla, puesto que una librería utilizada en el proyecto (Nut-js) ha dejado de ser de código abierto y para utilizarla hay que compilarla manualmente. A la hora de desarrollar otras imágenes, idealmente, sería necesario compilar esta librería o buscar alguna alternativa y modificar el código en consecuencia. Anexo II: Manual de usuario 88 88 Imagen 39: Contenedor creado. Si se pulsa el botón de la pequeña rueda dentada en la tarjeta del contenedor, aparece un formulario para editar el contenedor. En este modal se pueden editar todos los parámetros establecidos en la ventana de creación de contenedor. Si se pulsa el botón de Guardar, se aplicarán los cambios realizados y el contenedor se actualizará. Imagen 40: ventana de edición de contenedor. Adicionalmente, al pulsarse el botón de Eliminar Contenedor, el contendor correspondiente procederá a ser eliminado. En la tarjeta de cada contenedor, si se pulsa el botón de Conectar, se establecerá una conexión con el entorno remoto. La ventana que se muestre variará según el modo de conexión establecido, aunque su estructura será similar. En ambas pantallas se mostrará una gran sección que muestra el escritorio remoto o la consola de comandos, dependiendo del tipo de conexión. Además, aparece un menú desplegable que muestra dos opciones. 89 Imagen 41: Ventana de conexión de escritorio remoto Las opciones que aparecen son las siguientes: • Pantalla completa. Al pulsarse, muestra la ventana en pantalla completa. Si la ventana ya se encuentra ampliada, se deshace esta acción al pulsar el botón. • Desconectar. Permite cerrar la conexión con el entorno remoto. 7.3 Host De vuelta en la ventana principal (Imagen 36), en la parte superior de la ventana hay un switch que permite cambiar de modo cliente a modo host. El usuario host es aquel que comparte sus recursos con el resto de los usuarios. En esta ventana aparece al principio un único botón, el cual permite comenzar a compartir los recursos. Al pulsarlo, primero se comprueba que se precise del software necesario y en caso de no encontrarse esto, se procede a instalar todo el software necesario. Anexo II: Manual de usuario 90 90 Imagen 42: Mensaje de alerta de instalación de Vagrant. Imagen 43: Instalación Vagrant. Si el equipo presenta todo el software necesario, se muestra un menú que permite configurar los recursos a compartir con el resto de los usuarios. Los parámetros que se pueden configurar son: • Ubicación dentro del equipo donde se ubicarán los archivos relativos a la máquina virtual. • RAM que se desea compartir. • CPUs que se desean compartir. Al pulsar el botón de compartir, se arrancará la máquina virtual y se comenzarán a compartir los recursos con el resto del mundo. En caso de producirse cualquier error, se muestra al usuario un mensaje explicativo. Una de las causas principales de error es que el equipo no cuente con los recursos suficientes, o que no se encuentre instalado VirtualBox o Vagrant en el equipo. En el momento en el que se comienzan a compartir los recursos se pueden dejar de compartir pulsando el botón de Dejar de Compartir. La configuración realizada para compartir los recursos se almacenará en el sistema, de manera que la próxima vez que se compartan los recursos, no se preguntará de nuevo por esta configuración. 91 Imagen 44: Ventana de compartir recursos. En cuanto se hayan compartido los recursos una vez, al lado del botón de compartir aparece un nuevo botón, el cual al ser clicado muestra un menú que permite modificar la configuración de los recursos a compartir, pudiendo modificar los mismos parámetros que en la fase de creación de recursos compartidos. En este menú, además de los distintos parámetros de configuración, aparece la opción de eliminar los recursos compartidos, de manera que no solo se dejan de compartir, si no que se elimina toda la configuración almacenada acerca de los recursos compartidos. Referencias 92 92 REFERENCIAS [1] AWS, «What is virtualization?,» [En línea]. Available: https://aws.amazon.com/whatis/virtualization/?nc1=h_ls. [Último acceso: 04 06 2024]. [2] «What's a linux container?,» [En línea]. Available: https://www.redhat.com/es/topics/containers/whats-a- linux-container. [Último acceso: 06 06 2024]. [3] Vagrant, «Vagrant,» [En línea]. Available: https://www.vagrantup.com/. [Último acceso: 06 06 2024]. [4] «GPU Passthrogh,» [En línea]. Available: https://www.heiko-sieger.info/iommu-groups-what-you-need- to-consider/#What_is_IOMMU_and_why_do_I_need_it. [Último acceso: 07 06 2024]. [5] «What's RDP?,» [En línea]. Available: https://www.cloudflare.com/es-es/learning/accessmanagement/what-is-the-remote-desktop-protocol/. [Último acceso: 2024 06 07]. [6] «NAT,» [En línea]. Available: https://es.wikipedia.org/wiki/Traducci%C3%B3n_de_direcciones_de_red. [Último acceso: 2024 06 07]. [7] «Reverse SSH,» [En línea]. Available: https://jfrog.com/connect/post/reverse-ssh-tunneling-from-start-to- end/. [Último acceso: 07 06 2024]. [8] «webrtc.org,» [En línea]. Available: https://webrtc.org/. [Último acceso: 04 06 2024]. [9] «ICE Protocol,» [En línea]. Available: https://developer.liveswitch.io/liveswitch-server/guides/what-are- stun-turn-and-ice.html. [Último acceso: 04 06 2024]. [10] «Coturn GitHub,» [En línea]. Available: https://github.com/coturn/coturn. [Último acceso: 07 06 2024]. [11] «Electron,» [En línea]. Available: https://www.electronjs.org/docs/latest/. [Último acceso: 04 06 2024]. [12] RedHat, «What is a REST API?,» 31 07 2023. [En línea]. Available: https://www.redhat.com/es/topics/api/what-is-a-rest-api. [Último acceso: 04 06 2024]. [13] «Calico,» [En línea]. Available: https://docs.tigera.io/calico/latest/about/. [Último acceso: 13 06 2024]. [14] «Flannel,» [En línea]. Available: https://github.com/flannel-io/flannel. [Último acceso: 13 06 2024]. [15] «Flask,» [En línea]. Available: https://flask.palletsprojects.com/en/3.0.x/. [Último acceso: 21 06 2024]. [16] «Gunicorn,» [En línea]. Available: https://gunicorn.org/. [Último acceso: 21 06 2024]. [17] «Nginx,» [En línea]. Available: https://nginx.org/en/. [Último acceso: 21 06 2024]. [18] Wikipedia, «XVFB,» [En línea]. Available: https://es.wikipedia.org/wiki/Xvfb. [Último acceso: 19 06 24]. 93 [19] Xfce, «Xfce,» [En línea]. Available: https://www.xfce.org/. [Último acceso: 19 06 24]. [20] NutJS, «Nut-js,» [En línea]. Available: https://nutjs.dev/docs/getting-started. [Último acceso: 19 06 24]. [21] xtermjs, «xtermjs,» [En línea]. Available: https://xtermjs.org/. [Último acceso: 20 06 2024]. [22] D. O. Luis, «GitHub,» [En línea]. Available: https://github.com/Denis310599/sharepPC. [23] «Ubuntu,» [En línea]. Available: https://ubuntu.com/server/docs/install-and-configure-postgresql. [Último acceso: 30 06 2024]. [24] «www.json.org,» [En línea]. Available: https://www.json.org/json-es.html. [Último acceso: 04 06 2024].