---
title: "hackeps 2025: cómo quedamos segundos en el reto de eurecat"
description: "en la hackeps 2025 hicimos una plataforma de clústeres en aws y google cloud: por qué tiramos el go a mitad del hackatón y cómo funciona lo que salió de ahí."
date: 2026-10-08
author: "joel taylor pedrós"
lang: es
url: https://joeltaylor.business/blog/hackeps-2025-reto-eurecat
translations:
  ca: https://joeltaylor.business/blog/hackeps-2025
  en: https://joeltaylor.business/blog/hackeps-2025-eurecat-challenge
image: https://joeltaylor.business/_next/static/media/cover.1iurd3q0ku09g.webp
---

# hackeps 2025: cómo quedamos segundos en el reto de eurecat

![el panel de la aplicación con tres clústeres activos: uno de docker swarm con tres nodos y dos de k3s con dos nodos cada uno.](https://joeltaylor.business/_next/static/media/cover.1iurd3q0ku09g.webp)

la hackeps 2025 fue el 22 y el 23 de noviembre, y participé con maria aliet. es el hackatón de la escola politècnica superior de la universidad de lleida: novena edición, más de 230 inscritos y 24 horas. elegimos el reto de eurecat, que pedía una plataforma para desplegar y gestionar clústeres de computación híbridos, con máquinas en aws y en google cloud y dispositivos edge, todo desde el mismo sitio.

quedamos segundos. pero a mitad del hackatón no teníamos nada que funcionara, y lo borramos todo para volver a empezar.

## el plan con go, connect y protocol buffers

empezamos como si tuviéramos un mes. backend en go, con connect-go y protocol buffers, para tener los tipos compartidos de punta a punta entre el servidor y el cliente.

sobre el papel tiene sentido. en la práctica, cada endpoint nuevo pide una definición en el fichero `.proto`, generar el código de go y el del cliente e implementar el handler, antes de que ningún botón haga nada. en el ecuador del concurso teníamos un diagrama de arquitectura precioso y cero funcionalidades terminadas.

hicimos un hard reset y lo pasamos todo a t3:

- next.js y trpc, para tener tipos compartidos entre cliente y servidor sin generar código.
- postgresql con prisma, para poder cambiar el esquema sin quebraderos de cabeza.
- tailwind y shadcn, para tener una interfaz decente en minutos.

del go no queda nada en el repositorio, que empieza directamente con la versión nueva. lo que presentamos son 3.832 líneas de typescript nuestras en 60 ficheros, 1.494 de ellas en el servidor, más 5.745 de componentes de shadcn que no escribimos.

el recorte también se ve en el esquema de la base de datos. el primero tenía cuatro orquestadores posibles: k3s, nomad, docker swarm y kubernetes. una migración posterior quitó nomad y kubernetes, que no habría tiempo de hacer.

## cómo se crea un clúster en aws y google cloud

primero guardas las credenciales de cada proveedor: la clave de acceso y el secreto de aws, o el json de una cuenta de servicio de google cloud. luego creas un clúster. eliges docker swarm o k3s, le añades nodos con un proveedor y un tipo de instancia y marcas uno como máster.

al crearlo, una mutación de trpc guarda el clúster y los nodos en estado pending dentro de una transacción, y llama a `provisionCluster`, que hace el trabajo en tres etapas.

![diagrama de cinco pasos: crear las máquinas, esperar 15 segundos, pedir la ip pública, probar ssh cada 6 segundos hasta 50 intentos, e instalar swarm o k3s. el cuarto paso está destacado. debajo, el estado del nodo va de pending a provisioning, y acaba en active o failed.](https://joeltaylor.business/_next/static/media/aprovisionament.es.246410wksco_h.webp)

_el estado de cada nodo se guarda en la base de datos, y el panel lo muestra cuando lo recargas._

primero lanza las máquinas. el servidor genera un par de claves rsa de 4.096 bits por clúster. en aws, la clave pública entra por el user data, un script que la añade a `authorized_keys` cuando la máquina arranca. en google cloud, entra por los metadatos `ssh-keys` de la instancia. después espera 15 segundos y pide a cada proveedor la ip pública. por último se conecta por ssh, instala el orquestador y marca el nodo como activo.

pedir la ip pública a google cloud fue de lo último que añadimos, y hasta entonces ningún nodo de google cloud pasaba del estado provisioning.

node.js genera claves rsa, pero no sabe exportarlas en el formato de openssh que esperan aws y google cloud. así que hay una función escrita a mano que lee la clave pem byte a byte, extrae el módulo y el exponente y monta la clave `ssh-rsa` tal como la describe el rfc 4253.

la clave privada, en cambio, acaba en la base de datos en texto plano, junto a un comentario que dice "in a real app, this field should be encrypted :)".

## el polling por ssh

no teníamos tiempo para montar una cola de tareas, así que improvisamos. un bucle intenta conectarse por ssh a cada máquina nueva y ejecuta un `echo`. si falla, espera seis segundos y vuelve a probar. el nodo solo pasa a activo cuando responde. sin los logs, es esto:

```ts
async function waitForSSH(ip: string, user: string, privateKey: string, maxRetries = 50) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      await executeRemoteCommand(ip, user, privateKey, ["echo 'SSH Ready'"]);
      return;
    } catch (e) {
      await new Promise((r) => setTimeout(r, 6000));
    }
  }
  throw new Error(`SSH Connection timed out after ${maxRetries} attempts`);
}
```

el límite era de 20 intentos, unos dos minutos, y tuvimos que subirlo a 50, unos cinco.

todo esto pasa dentro de la misma petición http que crea el clúster, que no responde hasta que el último nodo ha terminado. para que no te quedes mirando el formulario, el botón dispara la mutación y te lleva al panel sin esperar la respuesta.

uno de los commits se llama "parallelisation of provisioning", pero el código sigue siendo secuencial. lo que cambió es el orden. antes, cada nodo se creaba y esperaba 15 segundos su ip. después, se lanzan todos, se espera 15 segundos una sola vez y se recogen todas las ips. un comentario dentro del mismo código lo admite: "for safety in a hackathon (rate limits), let's keep it sequential but fast".

desplegar una aplicación también pasa por ssh, siempre en el máster. con swarm, el servidor ejecuta allí `docker service create` con la imagen que le das. con k3s, codifica el manifiesto en base64, lo decodifica en la máquina y ejecuta `kubectl apply`. así el yaml llega entero, sin tener que pelearse con las comillas dentro de una orden de shell.

## lo que quedó a medias

el código también enseña lo que no llegó a funcionar:

- en swarm, cada nodo ejecuta `docker swarm init` por su cuenta, y con k3s solo se instala en el máster. los nodos no se unen entre ellos, porque ninguno hace `join`.
- los dispositivos edge se registran con una ip y un usuario, y se marcan como activos si tienen ip, sin conectarse a ellos.
- en aws hay una sola ami y un solo security group escritos en el código. la ami es la de us-west-2 y se usa en cualquier región, aunque cada ami solo existe en la región donde se creó.

el readme, escrito dos días después, también lista lo que no llegó. la parte de ia del reto, elegir proveedor y región a partir de una petición en lenguaje natural, quedó diseñada pero sin conectar. y la primera mejora de la lista es una cola con redis y bullmq que saque el aprovisionamiento de la petición http. es la pieza que el polling por ssh tuvo que sustituir.
