🕷️ Spin: una plataforma para microservicios en WebAssembly

Spin es una plataforma para el desarrollo de aplicaciones WebAssembly orientada a los microservicios. Está muy enfocada en mejorar la experiencia del desarrollo ya que nos permite ocuparnos solamente de la lógica de los casos de uso que queremos implementar, sin preocuparnos por la habilitación y el lanzamiento del microservicio.

Se basa en un modelo manejado por eventos y por el momento soporta peticiones HTTP y mensajes a través de canales en Redis.

Las aplicaciones en WebAssembly ofrecen varias ventajas, en gran parte gracias a su portabilidad, seguridad y rapidez. Spin aprovecha dichas ventajas y nos permite un desarrollo rápido de servicios que también pueden ser lanzados a la nube mediante Fermyon Cloud (por ahora en versión beta).

En este artículo veremos cómo crear un pequeño web scraper que nos permite extraer el título y la descripción de un sitio web utilizando Spin y posteriormente subirlo a la nube.

ℹ El código generado se encuentra hospedado en este repositorio.

Instalación de Spin y creación de una aplicación

La instalación para macOS es tan simple como descargar un binario desde su sitio web

; curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash

Posteriormente se puede mover este binario descargado a un lugar desde el cuál pueda ser ejecutado globalmente en línea de comandos, por ejemplo /usr/local/bin

; sudo mv spin /usr/local/bin/

Para más opciones o resolución de dependencias específicas en Linux les recomiendo ir a la documentación oficial.

Una vez instalado el binario correctamente podemos invocarlo para crear una aplicación que atenderá peticiones HTTP. Spin tiene a su disposición varias plantillas de proyectos con código y configuración inicial para poder comenzar a escribir servicios inmediatamente. En mi caso utilizaré la plantilla en Rust ya que es el lenguaje que prefiero.

# Al ser la primera vez hay que descargar las plantillas de aplicación disponibles
; spin templates install --git https://github.com/fermyon/spin

# Una vez descargadas creamos una app usando http-rust como plantilla y respondemos a las preguntas mostradas
; spin new http-rust webscraper

Project description: A webscraper using Spin
HTTP base: /
HTTP path: /...

# Ahora que la aplicación ha sido creada podemos navegar su directorio
; cd webscraper

Estructura y arquitectura de una aplicación Spin

Dentro del directorio de la aplicación se encuentra el código base para una librería en Rust, además se puede observar un archivo spin.toml, el cual define a una aplicación Spin. En este archivo se detallan los componentes WebAssembly que residen en la aplicación y el evento que los activa. Dicha relación está definida mediante trigger y component, como se detalla a continuación:

# extracto de spin.toml
[...]
trigger = { type = "http", base = "/" }
[...]

[[component]]
id = "webscraper"
source = "target/wasm32-wasi/release/webscraper.wasm"
[component.trigger]
route = "/..."

[...]

En este caso se define un servicio que, al recibir una petición HTTP en la ruta raíz, invocará al componente webscraper.wasm.

ℹ El archivo de configuración cuenta con mucho más opciones las cuales son detalladas en la sección correspondiente de la documentación oficial.

El componente que se menciona en la configuración reside en lib.rs, el cual por el momento cuenta con un código de ejemplo que contiene una versión del “Hola mundo”.

// lib.rs

#[http_component]
fn webscraper(req: Request) -> Result<Response> {
    println!("{:?}", req.headers());
    Ok(http::Response::builder()
        .status(200)
        .header("foo", "bar")
        .body(Some("Hello, Fermyon".into()))?)
} 

Como se puede observar la función webscraper está decorada con el atributo #[http_component]que identifica al punto de entrada del servicio. Además en su argumento recibe una petición web http::Request y retorna un mensaje mediante http::Response.

Es aquí donde nos enfocaremos para definir la lógica del servicio.

Creando el webscraper

El servicio que queremos crear opera de la siguiente manera

  1. Recibe una petición HTTP que contiene la dirección web que queremos inspeccionar en una estructura JSON. Esta petición es validada y transformada utilizando serde y serde_json.
  2. Verifica que la dirección sea una URL válida. Esto se realiza mediante el huacal url.
  3. Lanza una nueva petición HTTP a dicha URL para descargar el contenido del sitio web. Esta nueva petición se logra usando la SDK de spin.
  4. Analiza el cotenido mediante el huacal scraper buscando dos cosas:
    • El título del sitio web.
    • La meta-descripción del sitio web.
  5. Una vez identificado esto retorna la respuesta que contiene un mensaje JSON en donde se detalla esta información, si no existe simplemente retorna cadenas vacías.

Esta lógica se implementa a continuación:

#[http_component]
fn webscraper(req: Request) -> Result<Response> {
    let body = req.body().clone().unwrap_or_default();
    let payload: ScraperRequest = serde_json::from_slice(&body)?;

    let url = Url::parse(&payload.url)?;

    println!("Making request to {}", url);

    let res = spin_sdk::http::send(
        http::Request::builder()
            .method("GET")
            .uri(url.to_string())
            .body(None)?,
    )?;

    let response = match res.body() {
        Some(bytes) => {
            // some websites have invalid utf-8 content
            let html_doc = unsafe { std::str::from_utf8_unchecked(bytes) };
            let html = Html::parse_document(html_doc);
            let title_selector = Selector::parse("title").unwrap();
            let title = match html.select(&title_selector).next() {
                Some(title) => title.inner_html(),
                None => "".to_string(),
            };

            let desc_selector = Selector::parse(r#"meta[name="description"]"#).unwrap();
            let description = match html.select(&desc_selector).next() {
                Some(description) => description.value().attr("content").unwrap_or("").to_string(),
                None => "".to_string(),
            };

            ScraperResponse {
                title,
                description,
            }
        }
        _ => ScraperResponse {
            title: String::from(""),
            description: String::from(""),
        },
    };

    let res = serde_json::to_string(&response)?;

    Ok(http::Response::builder()
        .status(200)
        .body(Some(res.into()))?)
}

Levantando el servicio

Una vez creado el servicio es hora de construir el binario en WebAssembly. Spin ofrece el subcomando build para dicho objetivo, este comando se debe ejecutar en el directorio del proyecto.

; spin build

Cuando la compilación termina un archivo webscraper.wasm es generado y almacenado dentro del directorio release

; ls target/wasm32-wasi/release/*.wasm
target/wasm32-wasi/release/webscraper.wasm

Este mismo archivo es también mencionado en spin.toml como fuente del componente.

Por defecto un servicio spin no permite realizar peticiones a sitios web externos, lo cual impide que el scraper funcione ya que intencionalmente debe poder realizar peticiones a la URL que el cliente le indica.

Para permitir el correcto funcionamiento debemos añadir la opción allowed_http_hosts a la configuración de spin, en este caso declaramos (y aceptamos) "insecure:allow-all".

// spin.toml
[...]

[[component]]
id = "webscraper"
source = "target/wasm32-wasi/release/webscraper.wasm"
allowed_http_hosts = ["insecure:allow-all"]

Ahora podemos levantar el servicio usando el subcomando up 🙂

; spin up
Serving http://127.0.0.1:3000
Available Routes:
  webscraper: http://127.0.0.1:3000 (wildcard)

Comprobando el servicio

Una vez que nuestro servicio se está ejecutando podemos lanzar una petición HTTP para que obtenga la información que necesitamos.

; curl --location --request POST "http://localhost:3000" --data-raw '{ "url": "https://vasquezruiz.com" }'

{"title":"Raymundo Vásquez Ruiz","description":"Raymundo Vásquez Ruiz. Computer Engineer from Ixtepec, Oaxaca, México, living in Brussels, Belgium. Always trying to improve as Engineering Lead."}

Lanzando el servicio a la nube

Una aplicación Spin también puede ser publicada en la nube mediante Fermyon cloud. Al operar este servicio no tenemos que preocuparnos por ninguna infraestructura, una vez más confirmando el hecho de que la experiencia del desarrollador es la prioridad en este caso.

ℹ️ Por ahora Fermyon cloud está en versión beta. Lo cual implica que no hay ninguna garantía de servicio ni de compatibilidad hacia el futuro.

Para registrarse sólo se requiere una cuenta en Github y una aprobación por parte del equipo. Después de ser aprobado necesitamos seguir una serie de pasos para almacenar las credenciales que nos permitirán identificarnos con la nube cuando los servicios son lanzados. Todo esto se encuentra plenamente explicado en la documentación.

Una vez finalizado el registro el lanzamiento es tan simple como

; spin deploy
Uploading webscraper version 0.1.0+r8683ee72...
Deploying...
Waiting for application to become ready........ ready
Available Routes:
  webscraper: https://webscraper-sqgbedcw.fermyon.app (wildcard)

Y podemos ejecutar el mismo comando de prueba para verificar su funcionalidad

curl --location --request POST "https://webscraper-sqgbedcw.fermyon.app" --data-raw '{ "url": "https://vasquezruiz.com" }'
{"title":"Raymundo Vásquez Ruiz","description":"Raymundo Vásquez Ruiz. Computer Engineer from Ixtepec, Oaxaca, México, living in Brussels, Belgium. Always trying to improve as Engineering Lead."}

Cuando navegamos al panel de administración de Fermyon cloud encontramos una excelente interfaz con lo mínimo necesario para observar el comportamiento de nuestro servicio.

En este caso el servicio crea una instancia de nuestro binario WebAssembly cada vez que recibe una petición HTTP y esta instancia es desechada al terminar su vida útil.

Fermyon cloud - Panel

Nota: el servicio ha sido removido después de escribir esto.

Conclusión

Con una excelente experiencia de desarrollo en mente, compuesta de simplicidad de uso, plantillas de código y lanzamiento en la nube, Spin se convertirá en una excelente plataforma con la cual se podrán aprovechar las ventajas que el desarrollo y ejecución de módulos en WebAssembly ofrece.

En lo personal no me cabe duda de que el desarrollo de esta tecnología traerá grandes cambios a la forma en la que la industria del software entrega productos a sus clientes.