El juego es 100% estático (JS vanilla + HTML5 Canvas, sin build) servido por GitHub Pages.
Lo único que sale a la red es el chat con los linyeras: un fetch a nuestro proxy. Y ese
pedido atraviesa una infra Kubernetes self-hosted entera. Esto es el viaje completo de un mensaje,
de punta a punta. Nada es mágico ni una nube ajena: es fierro propio, y todo se declara por API.
Hay dos cosas distintas viajando: los estáticos del juego (HTML/JS/CSS) y el chat con IA. Los estáticos pueden servirse desde GitHub Pages (hoy) o desde tu propia infra (nginx en el cluster); el chat siempre sale por la infra self-hosted. Este es el mismo navegador, dos orígenes:
[ Navegador / GitHub Pages ] el juego (estático)
│ HTTPS fetch(llm-tormenta-solar.cybercirujas.club)
▼
[ DNS público + IP WAN ] A → IP de casa
│ :443
▼
[ HAProxy (borde, Mac mini) ] modo TCP · SNI passthrough
│ reenvía TCP por hostname (no termina TLS)
▼
[ Cilium Gateway API 192.168.178.200 ] ← acá TERMINA el TLS (Let's Encrypt)
│ cilium-envoy + HTTPRoute (hostname → Service)
▼
[ Service → Pod: tormenta-ai-proxy ] Node · CORS · personas · guardrails
│ POST /v1/chat/completions
▼
[ LiteLLM (el centralizador) ] pool de keys · fallback · ruteo
├──────────────┬──────────────┐
▼ ▼ ▼
[ OpenRouter ] [ GPU NVIDIA ] [ NPUs RK1 ]
nube · free HAMi + Ollama 4× inferencia local
Vanilla JS + Canvas, sin framework ni build, hosteado en GitHub Pages. El chat hace un
fetch POST con el NPC, tu mensaje y un poco de contexto. Tu API key es opcional:
por defecto pega contra nuestro proxy (gratis); si ponés una key de OpenRouter, queda solo en tu
navegador como override. La key "buena" del servidor nunca toca el cliente.
El dominio llm-tormenta-solar.cybercirujas.club resuelve a la IP pública de casa.
El certificado es de Let's Encrypt, emitido por cert-manager con desafío DNS-01 vía
acme-dns — así no hace falta exponer el :80 ni validar por HTTP. El cert se renueva solo.
El borde es lo más cirujeado de todo: una Mac mini G4 (PowerPC) corriendo OpenBSD —
sí, una máquina de ~2005 reciclada como router TLS. Ahí vive HAProxy en modo TCP con
SNI passthrough: mira el
req.ssl_sni del hello TLS y, según el hostname, manda el TCP crudo al backend que
corresponde — sin desencriptar nada (el TLS lo termina el gateway, más adentro). Varios dominios
comparten el mismo backend que apunta a la VIP del cluster. Acá ajustamos maxconn y
timeout para que las respuestas largas del LLM no se corten.
El tráfico entra al cluster Kubernetes por el cluster-gateway
(GatewayClass cilium), una VIP fija servida por LB-IPAM de Cilium. Acá termina el
TLS: un listener HTTPS por hostname presenta el certificado (el Secret que llenó cert-manager).
Es Gateway API, no Ingress: el routing es un recurso declarativo y estándar.
Un HTTPRoute matchea el hostname y enruta al Service del proxy. El plano de datos es cilium-envoy, que hace de reverse-proxy HTTP dentro del cluster. Sumar un dominio nuevo es, literalmente, agregar un HTTPRoute y un listener — sin tocar HAProxy más que la regla de SNI.
tormenta-ai-proxyUn servicio chico en Node (imagen propia, arm64). Hace tres cosas: pone los CORS para que GitHub Pages pueda llamarlo; guarda las personas (los system-prompts de cada linyera) del lado del servidor; aplica los guardrails (si el modelo tarda, devuelve el "la tormenta solar interfiere con el modelo" en vez de dejarte colgado). Después reenvía el pedido a LiteLLM con la key real, que jamás sale al navegador.
Un único endpoint OpenAI-compatible (/v1/chat/completions) que es el cerebro del
ruteo. Mantiene un pool de API keys, hace fallback entre modelos si uno falla o satura, y
decide a dónde mandar cada request según el model_name. Cambiar de "nube" a "fierro propio"
es cambiar un nombre de modelo — el juego no se entera de nada. Hoy el chat usa un modelo
free (familia Gemma) por defecto.
Detrás de LiteLLM hay tres destinos, intercambiables:
La idea: empezar gratis en la nube y, cuando convenga, mover el chat al hardware propio sin tocar ni el juego ni el proxy.
Hubble (de Cilium) muestra cada flujo de red L3/L4/L7 del cluster: se puede ver, en vivo, el pedido saliendo del proxy hacia LiteLLM y de ahí a la inferencia. Prometheus junta las métricas (LiteLLM expone requests, latencia, gasto, fallbacks) y Grafana las grafica. Si algo va lento o falla, se ve en un tablero, no a ciegas.
La imagen del proxy se buildea dentro del cluster con Kaniko orquestado por Argo Workflows (no hace falta un Docker daemon ni una CI externa), en un nodo arm64, y se empuja a un registry interno. El deploy es un Helm chart que crea todo lo declarativo: el HTTPRoute, el Certificate, y hasta un hook idempotente que suma el listener HTTPS al gateway compartido. Volver a levantarlo en otro cluster es correr un comando.
Nada de esto se hizo "a mano y a ver si anda": Gateway y HTTPRoute (Gateway API), Certificate y ClusterIssuer (CRDs de cert-manager), el build (CRD de Argo), el deploy (Helm values). Todo es un objeto versionado que se reaplica igual siempre. Eso es lo que hace que una infra casera sea seria: es reproducible.
Se va a agregar un bot de Telegram conectado a Hermes (un agente que ya corre en el mismo cluster) para manejar el juego desde el chat de Telegram: administrarlo, generar contenido nuevo y orquestar el mundo desde el celular. El juego pasa a tener un "panel de control" conversacional, sobre la misma infra.