La importancia de la web no ha hecho más que crecer desde su invención. Hace unos años era impensable poder trabajar únicamente con un navegador, siempre era necesario descargar e instalar aplicaciones de escritorio para poder ejecutar aplicaciones pesadas. No obstante, con el avance de la tecnología y la sofisticación tanto de los navegadores como de las tecnologías de cliente y servidor; el paradigma ha cambiado y nuestros navegadores ahora son auténticas navajas suizas. WebAssembly (WASM) tiene parte de culpa en todo esto, habiendo iniciado una tendencia para poder acercar las aplicaciones web al hardware del cliente con rendimientos casi nativos.

Todo estaba tranquilo hasta que en 2019, uno de los co-creadores de Docker, Solomon Hykes, comentó lo siguiente:

"Si WASM+WASI hubiera existido en 2008, no hubiéramos necesitado crear Docker. Tan grande es su importancia. WebAssembly en el servidor es el futuro de la computación”.

Evidentemente, una declaración tan firme que casi raya el sensacionalismo es algo que lanzó a mucha gente a subirse al carro de WebAssembly. Pero, ¿hasta qué punto es real esta afirmación?

En este post intentaremos aislarnos de la euforia colectiva para responder a lo que muchos os estaréis preguntando: ¿Realidad o moda? ¿Qué es WASI y por qué parece tan revolucionario? ¿Qué tiene que ver una tecnología para acelerar aplicaciones en el navegador con el servidor? ¿Y qué pinta Docker en todo esto?

Empecemos por el principio.

¿Qué es WebAssembly?

La definición formal de WebAssembly (WASM para los amigos) es la de un lenguaje de bajo nivel, parecido al ensamblador, especialmente pensado para la Web. Define un conjunto estructurado de instrucciones propias sobre una máquina virtual basada en pilas (de funciones, operandos, etc.). Al estar tan cerca de los lenguajes primitivos como ensamblador, la traducción del conjunto de instrucciones a lenguaje máquina es un proceso ligero y directo.

Si intentamos definirlo de una forma más llana, WebAssembly es un lenguaje de bajo nivel, completamente independiente de la arquitectura del sistema (ARM, x86, x64, etc.) que nos permite desarrollar una aplicación autocontenida con lenguajes muy potentes como Rust, C, C++; compilarlo a WASM (permite compilaciones AOT y JIT) y distribuir el bytecode para ejecutarlo libremente.

El bytecode generado por el resultado de la compilación se ejecuta dentro de un sandbox, en una máquina virtual incorporada en todos los navegadores modernos, y nos permite ejecutar programas mucho más potentes sin tener que recurrir a lenguajes más ineficientes como Javascript. ¿Quiere decir que es un sustituto de Javascript? No. WASM no puede modificar el DOM de una web, por ejemplo. De la misma manera, el navegador solo “entiende” Javascript, por lo que WASM incorpora una API de comunicación para poderse invocar entre ellos.

A continuación un pequeño ejemplo:

fetch('module.wasm')
.then(response => response.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes))
.then(module => {
    // Get the exported function from the module
    const add = module.instance.exports.add;

    // Call the function and print the result
    const result = add(1, 2);
    console.log(result);
});

En este punto, WebAssembly nos permite disponer de:

Seguro que ya se nos ocurren varios casos de uso que mejorarían sustancialmente con esta tecnología: videojuegos, realidad virtual, realidad aumentada, inteligencia artificial, etc. Además, con el paso del tiempo, WASM ha evolucionado de mano de la comunidad, y ahora la mayoría de lenguajes tienen mecanismos para compilar aplicaciones a WASM (algunos todavía muy rudimentarios), no solo los anteriormente mencionados.

Si sois del tipo escéptico, os invito a leer este interesante artículo sobre cómo Google migró Earth a WebAssembly para mejorar la experiencia en todos los navegadores.

¿Y quién desarrolla todo esto? WebAssembly es desarrollada activamente por comunidades de desarrollo tanto independientes como por parte de los grandes actores del mundillo: Mozilla, Google, Microsoft, RedHat, etc. A nivel de soporte, esponsorización y estandarización, WASM es gestionado por el W3C WebAssembly Community Group.

Tengo flashbacks de Java

Y es lógico, lo que propone WebAssembly es muy cercano al motto de Java “write once, run everywhere”. Podríamos incluso decir que se consideran herederos espirituales de esta filosofía. De hecho, WebAssembly podría solaparse con GraalVM en cuanto al objetivo común de plantear una VM políglota en su naturaleza (el bytecode de Java no lo es). Sin embargo, mientras que GraalVM plantea una API y lenguajes de alto nivel para conseguir esto, WebAssembly define directamente un conjunto de instrucciones, consiguiendo mayor simplicidad, universalidad y eficiencia. Para aquellos que siguen viendo este punto como algo muy redundante, recomiendo la lectura de WebAssembly for the Java Geek - JVM Advent.

Al otro lado de la mesa, WebAssembly es una representación de bajo nivel, pensada para ser políglota desde el principio. Es un esfuerzo para permitir libertad total al desarrollador al utilizar el lenguaje de su preferencia que luego se compila a un lenguaje común.

¿Qué ocurre entonces con herramientas tan útiles como el Garbage Collector y la gestión general de la memoria? WebAssembly no implementa en su especificación instrucciones y mecanismos que permitan una gestión transparente de la memoria como sí ocurre en la JVM. Por ejemplo, actualmente no existe una implementación completa del GC, pero existen propuestas oficiales sobre las que se está trabajando de forma activa.

WebAssembly en el servidor

Demos el salto al servidor. WebAssembly nos propone un sandbox, donde todos los recursos del sistema que se van a utilizar deben ser declarados de antemano. Adicionalmente, también nos propone una representación abstracta y portable. Por lo tanto, ¿podríamos llegar a ejecutar aplicaciones autocontenidas en el servidor con WASM?

Esto plantea varios retos:

En un sistema operativo UNIX, los programas que ejecuta un usuario lo hacen en el espacio de usuario y todas las llamadas al kernel son gestionadas por una interfaz del sistema (siendo el mayor exponente POSIX), que hace de intermediario entre el programa y el Kernel. Por ejemplo, es esta interfaz la que revisará que un usuario tenga los permisos necesarios para abrir un fichero.

Por lo tanto, parece lógico que la solución al primer y segundo punto sea una interfaz del sistema propia para WASM, que no requiera de una máquina virtual como la del navegador y que hable directamente con el Kernel del sistema. A raíz de ello, nace WASI (WebAssembly System Interface) por iniciativa de Mozilla.

WASI es el que permite el cambio de paradigma. Los módulos compilados en WASM ya no requieren de un entorno virtualizado completo como en el navegador, solo de un runtime que implemente WASI. De esta forma, podemos ejecutar aplicaciones en un entorno sandbox de forma transparente a la arquitectura de la CPU y el sistema operativo. Gracias a WASI, mantenemos rendimientos casi nativos a la plataforma y aprovechamos la universalidad del binario.

WASI además es modular. Cada plataforma hace las cosas de una forma distinta, algunas se ponen de acuerdo para cosas básicas, mientras que otras son espíritus libres. Con el objetivo de hacer esta interfaz lo más reusable, genérica y extensible posible, su arquitectura interna está creada a partir de módulos. Existe una implementación de wasi-core que contiene las llamadas al sistema más básicas (red, ficheros, etc.), mientras que funcionalidades más complejas (operaciones sobre blockchain, IoT, ML, etc.) son provistas por módulos más complejos independientes. Por ejemplo, wasi-nn.

Por último, y para solventar el tercer punto, nacen los runtime de bajo nivel (wasmer, wasmtime, wasmedge, etc.), que implementan WASI y son los encargados de traducir las llamadas al sistema a la arquitectura del host.

¿Seguro que es seguro?

Hemos mencionado varias veces que es más seguro, pero ¿por qué?

Pongamos un ejemplo sencillo de un programa que quiere abrir un fichero. Un SO linux por ejemplo, decide si ese programa puede hacerlo en función de los permisos del usuario que ejecuta el programa. Podríamos afirmar que el SO se fía del código que el usuario ejecuta porque confía en el usuario.

Por otro lado, a lo mejor ese programa que se supone que solo lee ficheros utiliza una librería comprometida y por debajo está accediendo a la configuración de red para abrir un puerto. El Kernel nuevamente solo revisa los permisos del usuario y le permitirá acceder a esa configuración.

En el caso de WASI, este deniega por defecto todo. Debemos declarar de antemano qué llamadas al sistema son necesarias. Aunque ejecutemos una librería remota de un tercero que puede estar comprometida, si nuestro código solo declara las llamadas al sistema para abrir ficheros, será completamente incapaz de acceder a la configuración de red, por mucho que el usuario tenga los privilegios para ello.

Hablemos de contenedores

Llegados a este punto, tenemos una forma de ejecutar aplicaciones autocontenidas, portables, con rendimientos casi nativos y limitadas por un sandbox. Imaginemos estas aplicaciones como cajitas… casi parecen contenedores, ¿verdad? Pero antes de ahondar en esta línea de pensamiento, permitidme un breve repaso de cómo funcionan ahora mismo los contenedores que usamos en nuestro día a día.

Cuando hablamos de contenedores, normalmente todos pensamos en herramientas como Docker o containerd. Es decir, pensamos en Container Runtime Interfaces (CRI), y… ya. Normalmente, no se suele profundizar más en cómo se gestionan los contenedores. Estos CRI son los encargados de iniciar, eliminar, inspeccionar o ejecutar comandos en dichos contenedores de forma transparente al desarrollador. No obstante, si abrimos la caja de Pandora, nos encontramos algo parecido al siguiente diagrama:

Diagrama de contenedores

Contenedores sin contenedores

Volviendo al tema que tenemos entre manos, la ejecución de WASM en el servidor se parecía demasiado al concepto de contenedor: una forma de distribuir y ejecutar aplicaciones con todas sus dependencias incluidas de forma sencilla y agnóstica al host. Es más, con WASM no sería necesario introducir un sistema operativo virtualizado como en el caso de los contenedores. Esto abría la puerta a algo que, en principio, podría ser una evolución del concepto de contenedor tal y como lo conocemos.

Por desgracia, viendo el enorme ecosistema de herramientas y metodologías que se han desarrollado en torno a los contenedores, es difícil plantear una nueva alternativa (por beneficiosa que sea) que además en aquel momento era muy incipiente.

Aquí es donde entran Microsoft, Docker y otros grandes actores para crear runwasi, un shim que permite a containerd invocar módulos de WASM como si fueran contenedores. La figura anterior, por tanto, evoluciona a lo siguiente:

Runwasi

Observamos lo siguiente:

Como bajo el punto de vista de containerd las órdenes son las mismas, y es el shim el encargado de traducir las instrucciones, los módulos de WASM se ejecutan como contenedores y responden a las órdenes habituales de run, exec, logs, etc. Por tanto, hemos conseguido tener contenedores sin contenedores. Realmente es como si estuviéramos engañando a containerd. Esta aproximación es la que Docker presentó en su famosa demo de Docker + wasm y la que promueve el propio Microsoft.

En última instancia, si tenemos la forma de tratar un módulo de WASM como si fuera un contenedor de forma estándar, entonces podremos usar todo el ecosistema anteriormente citado. Esto da un impulso enorme a la posibilidad de adoptar WebAssembly dentro de los sistemas ya existentes. Y por supuesto, también en Kubernetes.

Un pasito más

La aparición de runwasi abre la veda de caza en lo que se refiere al tratamiento de módulos de WASM como contenedores. Pero al mismo tiempo planteaba el problema de tener que instalar dos runtime distintos con shims distintos que hay que mantener y gestionar. En Kubernetes, por ejemplo, es necesario registrar el shim y el runtime en containerd, definir el RuntimeClass y referenciarlo en el Deployment de la aplicación.

Por suerte, existen otras alternativas al uso de runwasi que están apoyadas también por grandes actores. Es el caso de youki (Rust) o crun (RedHat). En vez de shims, estas herramientas son runtimes OCI de bajo nivel y aúnan la capacidad de levantar tanto contenedores tradicionales como de ejecutar módulos de WASM. Simplificando su funcionamiento, estos runtimes deciden en base a las anotaciones de la imagen qué plataforma de ejecución es la correcta. Al detectar la anotación, tanto youki como crun omitirán la inicialización del contenedor, extraerán el binario WASM y ejecutarán directamente el módulo.

El diagrama anterior vuelve a evolucionar:

Evolución del diagrama.

Ahora sí parece mucho más limpio y lineal. Pero, y para terminar de rematar este apartado, todo esto no tendría sentido si no pudiéramos definir imágenes OCI para ejecutar módulos de WASM (vaya... suena como si de verdad estuviéramos desarrollando contenedores). Afortunadamente, sí es posible:

# Descargamos el ejemplo
$ git clone https://github.com/second-state/wasm-learning
$ cd wasm-learning/cli/wasi
# Compilamos la aplicación en wasm32-wasi
$ rustup target add wasm32-wasi
$ cargo build --target wasm32-wasi --release
# El bytecode resultante se encuentra en target/wasm32-wasi/release/wasi_example_main.wasm
$ chmod +x target/wasm32-wasi/release/wasi_example_main.wasm
# Creamos el Dockerfile
$ cat <<EOF > Dockerfile          
FROM scratch
ADD wasi_example_main.wasm /
CMD ["/wasi_example_main.wasm"]
EOF
# Construimos la imagen con la anotación
$ buildah build --annotation "module.wasm.image/variant=compat-smart" -t wasi_example .

Conclusiones

Todo lo bueno termina, pero hagámoslo a lo grande y sobre todo, volvamos a las preguntas iniciales. En primer lugar, uno podría pensar que parece mucho lío crear contenedores de WASM. Sí, es más ligero, pero... ¿realmente merece la pena? La realidad es que obtenemos varias ventajas:

Igual que no es oro todo lo que reluce, WebAssembly no es la panacea para solventar todos los problemas de la computación moderna. La tecnología es demasiado joven y todavía debe de madurar para poder ser realmente una alternativa productiva. Sí es verdad que WebAssembly para lenguajes como C/C++ o Rust es extremadamente sólida y madura, pero si vamos a lenguajes como Java, Python o Javascript encontraremos algunos problemas con librerías estándar, gestión de memoria, estructuras de datos, dependencias, implementación de tipos, etc. Una reflexión interesante la podemos encontrar en este post sobre los problemas de WebAssembly con lenguajes más allá de C y Rust.

Para dar un poco de optimismo en este sentido podemos tomar Python como ejemplo y ver cómo se está moviendo la comunidad a su alrededor:

Para ponerle el lazo al post, y desde la humilde visión del autor, WebAssembly es una tecnología con muchísimo futuro. Muchos grandes actores han intervenido en todas sus fases del desarrollo y esponsorizan activamente todos los proyectos asociados. Adicionalmente, se ha visto una clara ventaja a la hora de aprovechar mejor otro tipo de arquitecturas como ARM que son muchísimo más baratas tanto en consumo como en coste. Incluso compañías como Microsoft apuestan por esta tecnología con soporte oficial de wasm en nodepools de Azure Kubernetes Service (preview).

Con todo ello, no debemos olvidar que WASM en el servidor aún tiene mucho camino por recorrer y actualmente solo cubre algunos casos de uso muy específicos (siempre desde un punto de vista de aplicaciones productivas). En definitiva, estamos presenciando el crecimiento de una tecnología que dará muchísimo que hablar en pocos años.

Espero que el artículo haya sido de vuestro interés. ¡Hasta más ver!

PD: Si después de este artículo, os ha picado el gusanillo y queréis mancharos las manos, os recomiendo los excelentes posts de Nigel Poulton sobre WASM, WASI y WASM+Docker/Kubernetes.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.