Saltar al contenido
cloud-webv0.1.0
Documentación

Operación

Backups

Todo el estado del plano de control está en /var/lib/cloudweb:

Archivo Contenido
cloudweb.db apps, despliegues, usuarios, configuración
master.key clave que cifra los secretos — sin ella el resto no se puede descifrar
ca.key, ca.crt CA del clúster (si se pierde, los nodos deben volver a unirse)
admin.token token de administración de arranque

Para un backup consistente copiá el directorio completo con el servicio detenido (systemctl stop cloudweb-server), o usá sqlite3 cloudweb.db ".backup copia.db" más una copia de master.key, ca.key y ca.crt. Guardá el backup cifrado: contiene claves. Para restaurar, copiá los archivos a un servidor nuevo con el mismo --public-host y reiniciá el servicio.

Las imágenes están en el volumen Docker cloudweb-registry-data del nodo del plano de control; se pueden reconstruir con un redeploy.

Actualizaciones

Volvé a ejecutar el instalador:

curl -fsSL https://cloudweb.zondacloud.com/install.sh | sudo sh

Descarga los binarios nuevos, verifica sus checksums, los reemplaza de forma atómica y reinicia los servicios, conservando datos y configuración. Antes de aplicar migraciones el servidor guarda una copia (cloudweb.db.bak-vN). Para volver atrás: restaurá la copia y el binario anterior. En los workers volvé a ejecutar el instalador con --join … (o simplemente el instalador sin opciones si ya están unidos).

Configuración del servidor

/etc/cloudweb/server.env (reiniciar con systemctl restart cloudweb-server):

Variable Descripción
CLOUDWEB_PUBLIC_URL URL pública del dashboard (redirect OAuth y webhooks)
CLOUDWEB_PUBLIC_HOSTS nombres/IPs con que los nodos alcanzan al servidor (certificado del listener de agentes)
CLOUDWEB_BASE_DOMAIN dominio base de las apps
CLOUDWEB_INGRESS_HOST nombre estable de ingress (p. ej. ingress.zondacloud.com), destino recomendado del CNAME de los dominios propios; ver Dominios propios
CLOUDWEB_TLS_MODE, CLOUDWEB_ACME_EMAIL modo TLS y contacto Let's Encrypt
CLOUDWEB_REGISTRY host:puerto del registro de imágenes interno

Solución de problemas

Síntoma Qué mirar
El nodo no se une token vencido o usado; huella --ca-sha256 incorrecta; puerto 8443 bloqueado
Agente rechazado (control plane rejected this agent's protocol version) actualizá el lado indicado: agente con node.sh en el nodo, o el plano de control; ver protocolo
Un nodo drenado no vuelve a recibir instancias Nodos → Reactivar (o POST /api/nodes/{id}/undrain)
Los despliegues fallan con no node can host the instance (...) el mensaje nombra cada nodo y el motivo: draining (reactivalo), registry unreachable (el nodo no alcanza el registro: revisá la malla con sudo cloudweb-agent mesh status en ese nodo) o unreachable (sin latidos)
Una app muestra reemplazos fallidos que se repiten y luego dejan de intentarse al reprogramar una instancia fuera de un nodo drenado o caído, si el reemplazo falla se espera cada vez más (30 s, 1 min, 2 min… hasta 15 min) y tras 5 fallos seguidos se deja de intentar; la instancia actual sigue corriendo. El motivo queda en el historial del último intento. Resolvé la causa (típicamente la malla del nodo destino) y disparás un despliegue nuevo para empezar de cero
Un nodo en WSL2 queda degradada o nunca conecta la malla con la red NAT de WSL2 su endpoint es una IP interna que otras máquinas no alcanzan; ver Nodos en WSL2
Nodo unreachable journalctl -u cloudweb-agent; UDP 51820 entre nodos; hora del sistema
Build falla los logs del despliegue; si no hay Dockerfile ni lenguaje reconocido, agregá un Dockerfile
No llegan los redeploys el webhook en GitLab (Settings → Webhooks): la URL debe ser alcanzable desde GitLab
Sin HTTPS en una app el DNS debe apuntar a un nodo de ingress y los puertos 80/443 estar abiertos
502 en una app la app debe escuchar en $PORT y en 0.0.0.0

Logs: journalctl -u cloudweb-server y journalctl -u cloudweb-agent; las apps con docker logs cw-<app>-<n> o desde el dashboard.

Dominios propios y el nombre de ingress

Un cliente que trae su dominio crea un registro DNS en su propia zona. Si ese registro es un A a la IP de tu nodo y la IP cambia (conexión residencial, migración), cada cliente tiene que editar su DNS y vos no podés arreglarlo. El nombre de ingress evita eso:

  1. Elegí un nombre que controles, por convención ingress.<dominio-base>, y apuntalo a tus nodos de ingress (registro A con las IPs públicas, o DDNS si la IP es dinámica).
  2. Configurá CLOUDWEB_INGRESS_HOST=ingress.zondacloud.com en /etc/cloudweb/server.env y reiniciá cloudweb-server. Es opcional: sin él se recomienda el host de cada app.
  3. Los clientes crean un CNAME de su subdominio a ese nombre. Si tu IP cambia, actualizás un registro. Para el dominio raíz siguen necesitando un A (o ALIAS/ANAME).

Los dominios que ya existían con CNAME al host de la app, o con registros A a tus IPs, siguen activos: activar el nombre de ingress no cambia ninguno.

Qué comprobar:

  • dig +short ingress.zondacloud.com devuelve las IPs de tus nodos de ingress.
  • Desde otra red (datos del celular, no tu Wi-Fi: muchos routers no dejan que una máquina alcance su propia IP pública): curl -kI https://<dominio> responde. Los puertos 80 y 443 de ese nodo tienen que estar abiertos hacia internet.
  • Con CLOUDWEB_TLS_MODE=internal el certificado es de la CA local de Caddy y los navegadores lo marcan como no confiable; para dominios de clientes hace falta auto (Let's Encrypt).

Límites:

  • Cloudflare como proxy (nube naranja) en el nombre de ingress o en el dominio del cliente: Cloudflare recibe en 80/443 y reenvía a tu origen. Sirve cuando tu proveedor de internet bloquea esos puertos, pero un dominio detrás del proxy no muestra el CNAME ni tus IPs, así que la verificación lo deja pendiente.
  • El dashboard avisa cuando una dirección de ingress es privada o de CGNAT (no se alcanza desde internet) o cuando un nodo se anuncia por un nombre (su IP puede cambiar). Son avisos: no bloquean agregar ni verificar un dominio.
  • La plataforma no retira del DNS a un nodo caído ni balancea por sí misma: con varios nodos de ingress el reparto es el del DNS.
  • Nombres reservados: no se aceptan como dominio de una app el dominio base y todo lo que está bajo él, el nombre de ingress ni los nombres propios del plano de control (URL pública, instalador de nodos, agentes).

Migrar la plataforma a otro dominio (detrás de un túnel de Cloudflare)

Ejemplo: el dashboard pasa de cp.tukit.uk a console.zondacloud.com y el instalador de cw.tukit.uk a cloudweb.zondacloud.com, mientras agents, ingress y el dominio base de las apps no cambian. Hacelo en este orden y dejá los nombres viejos funcionando durante la transición.

  1. Túnel. En el archivo del túnel (~/.cloudflared/<nombre>.yml) agregá los nombres nuevos antes de la regla comodín, porque gana la primera que coincide:

    ingress:
      - hostname: console.zondacloud.com      # dashboard
        service: http://localhost:8080
      - hostname: cp.tukit.uk                  # alias durante la transición
        service: http://localhost:8080
      - hostname: cloudweb.zondacloud.com      # instalador (la app `cw`)
        service: https://localhost:443
        originRequest:
          noTLSVerify: true
      - hostname: "*.tukit.uk"
        service: https://localhost:443
        originRequest:
          noTLSVerify: true
      - service: http_status:404
    

    Reiniciá cloudflared. El DNS de cada nombre nuevo es un CNAME proxied al túnel (<id>.cfargotunnel.com): cloudflared tunnel route dns <túnel> console.zondacloud.com lo crea, pero necesita el cert.pem de esa zona (cloudflared tunnel login y elegir zondacloud.com); también se crea a mano en el panel de Cloudflare.

  2. El instalador como app de la plataforma. El nombre cloudweb.zondacloud.com está fuera del dominio base, así que se agrega como dominio propio de la app cw (Dominios → Agregar dominio). Siendo administrador queda activo sin verificar el DNS (de confianza): detrás de un túnel el DNS público responde con IPs de Cloudflare y la verificación nunca pasaría. Un usuario común no puede registrar ese nombre.

  3. Servidor. En /etc/cloudweb/server.env:

    CLOUDWEB_PUBLIC_URL=https://console.zondacloud.com
    CLOUDWEB_NODE_INSTALL_URL=https://cloudweb.zondacloud.com/node.sh
    

    y sudo systemctl restart cloudweb-server. Las sesiones abiertas en el dominio viejo no pasan al nuevo: hay que volver a entrar.

  4. Apps OAuth. Actualizá, con el dominio nuevo:

    • GitLab (Applications): Redirect URI https://console.zondacloud.com/api/auth/callback.
    • GitHub App: Callback URL https://console.zondacloud.com/api/auth/callback y Webhook URL https://console.zondacloud.com/api/webhooks/github.

    Después probá iniciar sesión desde el dominio nuevo.

  5. Comprobá que responden https://console.zondacloud.com, https://cloudweb.zondacloud.com/node.sh y, todavía, https://cp.tukit.uk y https://cw.tukit.uk.

Qué esperar del alias y de lo que ya existe:

  • Los webhooks de GitLab ya registrados en cada app apuntan a la URL vieja (cp.tukit.uk). Siguen entregando mientras ese nombre responda. Una app nueva, o una que se vuelva a conectar, registra el webhook con la URL nueva.
  • Los comandos de unión que ya se copiaron usan el instalador viejo (cw.tukit.uk): sigue sirviendo lo mismo.
  • No hace falta tocar los nodos: hablan con agents.<dominio>, que no cambia.
  • Cuando ya no quede nada apuntando a los nombres viejos, quitá cp.tukit.uk y cw.tukit.uk del túnel.

Cambiar el dominio base de las apps

El dominio base (CLOUDWEB_BASE_DOMAIN) es el sufijo de la dirección por defecto de cada app (<app>.<dominio-base>). Cambiarlo no mueve datos, pero las direcciones por defecto cambian: una app que estaba en web.tukit.uk pasa a web.zondacloud.com, y la vieja deja de enrutarse. Pasos, en este orden:

  1. Túnel. Agregá el comodín del dominio nuevo después de las reglas específicas (console, cloudweb, los dominios de clientes), que ganan por orden:

    - hostname: "*.zondacloud.com"
      service: https://localhost:443
      originRequest:
        noTLSVerify: true
    

    Reiniciá cloudflared. El comodín no alcanza al dominio raíz (zondacloud.com): ese nombre y www tienen su propio destino.

  2. DNS. En la zona de Cloudflare, un CNAME comodín * hacia el túnel (<id>.cfargotunnel.com, con proxy). Los registros explícitos (raíz, www, console, cloudweb) tienen prioridad sobre el comodín. El certificado universal de Cloudflare cubre *.dominio de primer nivel, que es lo que usan las apps.

  3. Servidor. En /etc/cloudweb/server.env: CLOUDWEB_BASE_DOMAIN=zondacloud.com, y sudo systemctl restart cloudweb-server.

  4. Comprobá una app: curl -sI https://<app>.zondacloud.com.

Qué tener en cuenta:

  • Todo lo que cuelga del dominio base queda reservado: ningún usuario puede registrarlo como dominio propio, y los nombres de app como console, cloudweb, agents, ingress o www no se pueden elegir (para no quedarse con las direcciones de la plataforma). Un dominio propio que ya existía bajo el nuevo dominio base (como cloudweb.zondacloud.com en la app cw) sigue activo.
  • agents.<dominio> y el nombre de ingress no se mueven solos: los nodos ya unidos se conectan a agents.tukit.uk, y cambiarlo exige volver a emitir el certificado del servidor y actualizar los nodos. Mientras tanto siguen donde estaban.

Usuarios que no son administradores: límites y contenedores restringidos

Una cuenta que no es administrador ejecuta código de terceros en tus nodos. Hoy hay estas protecciones, y es importante saber cuáles faltan:

  • Tope de apps por cuenta (por omisión 3; en Ajustes → Registro de usuarios → Límites por cuenta, 0 = sin tope). Los administradores no tienen tope. Al alcanzarlo, la API responde 403 con {"code":"quota_exceeded","quota":"apps","limit":3,"used":3}.
  • Memoria y CPU por contenedor (hoy los valores por defecto del scheduler, aplicados con --memory y --cpus).
  • Perfil restringido para las apps de usuarios comunes (harden en la orden de despliegue, que el agente aplica a docker run): --cap-drop ALL (sin capacidades de Linux salvo escuchar en puertos bajos), no-new-privileges, --pids-limit 512, --ulimit nofile=4096:4096 y sin swap. Un agente anterior lo ignora. Las apps de administradores (como la propia landing) no se restringen.
  • Sin buildpacks: los builds con buildpacks montan el socket de Docker del nodo (equivale a ser root en él), así que solo los administradores pueden usarlos. Un usuario común necesita un Dockerfile o un sitio estático; si no, el despliegue falla con un mensaje que explica qué agregar. Los builds de Dockerfile tienen tope de tiempo y de memoria y no ven el socket.
  • Nombres reservados y lista bloqueada para las apps de usuarios comunes, y dominios propios que no pueden ser de la plataforma.

Lo que todavía no existe (y por eso el registro abierto a cualquiera sigue detrás de su compuerta, que exige cuotas completas y un nodo endurecido):

  • Aislamiento de red: un contenedor de usuario alcanza hoy la LAN del nodo, la malla WireGuard y el registro de imágenes (10.77.x.x:5000, sin autenticación). No hay reglas de egreso.
  • Builds sin tope de tamaño: el docker build de un usuario tiene límite de tiempo y de memoria, pero no de tamaño del contexto ni de la imagen, y corre con la red por defecto del nodo.
  • Cargas de usuarios solo en nodos dedicados: el scheduler puede poner la app de un usuario en cualquier nodo, incluida tu propia máquina.
  • Cuotas de memoria/CPU por usuario, límites de tasa y suspensión de cuentas.

Mientras falten, no abras el registro a desconocidos en máquinas donde hay datos que te importan: el modo recomendado es invite o las reglas de acceso (cambio add-access-rules).

Seguridad

  • El token de administrador (admin.token) es una credencial de arranque equivalente a root del clúster: protegela y, una vez creado el administrador con GitLab, usá preferentemente la sesión. La pantalla de acceso no muestra el formulario del token cuando ya hay un proveedor listo: se revela escribiendo admin en la página (fuera de un campo de texto) o abriendo /?admin. Es solo para no dejarlo a la vista: lo que protege es el token, no esa palabra. Si la instancia no tiene ningún otro modo de entrar (sin proveedor configurado, o con el setup pendiente), el formulario se muestra solo, porque si no nadie podría configurarla.
  • Los builds ejecutan el código de los proyectos conectados con límites de memoria y tiempo, pero asumen proyectos de confianza: no conectes repositorios que no controlás.
  • Publicá el dashboard detrás de HTTPS (por ejemplo, como una app más del propio clúster).

CI con un runner propio (sin gastar minutos compartidos)

Los pipelines usan imágenes Docker (golang, node, release-cli), así que corren en cualquier máquina con Docker. La imagen base del runner (la de los trabajos que no declaran image:) es ubuntu:24.04 y se cambia con --image (o --runner-image en node.sh); el dashboard tiene un campo opcional para eso. Si se acaban los minutos de los runners compartidos de GitLab, se puede registrar un runner propio:

Desde el dashboard: Ajustes → Runners de CI (GitLab) arma el comando con tu token y permite descargar el script. El propio plano de control lo sirve en /api/runners/gitlab/install.sh (sin sesión, como node.sh), ya con la URL de GitLab de la instancia, así que un nodo lo puede bajar directo con curl. El token solo se escribe en el navegador y en el nodo: no pasa por el servidor.

  1. En GitLab, en el grupo (para que sirva a todos sus proyectos): Build → Runners → New group runner. Dejá Run untagged jobs marcado y copiá el token (glrt-…).
  2. En la máquina (necesita un Docker Engine propio, no Docker Desktop):
    curl -fsSL https://TU_DASHBOARD/api/runners/gitlab/install.sh | sudo sh -s -- --token glrt-XXXX
    
    (o sudo sh scripts/gitlab-runner.sh --token glrt-XXXX desde una copia del repositorio) Instala gitlab-runner desde el repositorio oficial, lo registra con el executor docker y lo deja como servicio. Es idempotente.
  3. En Settings → CI/CD → Runners de cada proyecto, apagá "Enable instance runners", para que los jobs esperen a tu runner en lugar de intentar (y fallar) en los compartidos.

Seguridad: quién puede correr código en tu máquina

Un job ejecuta el código del repositorio dentro de un contenedor, y el usuario que corre el runner suele pertenecer al grupo docker (equivale a root). Por eso:

  • Marcá el runner como Protected al crearlo: solo corre pipelines de ramas y tags protegidos. Protegé la rama por defecto y el patrón de tags v* (Settings → Repository → Protected tags), que es lo que usan los releases.
  • cloud-web-node es público: sin runner protegido, un merge request de un fork ejecutaría su código en tu máquina. En Settings → CI/CD → General pipelines desactivá los pipelines de merge requests de forks.
  • El script no activa privileged ni monta el socket de Docker: un job no controla el Docker del equipo. Consecuencia: el job e2e-docker (docker-in-docker) falla; está marcado allow_failure y esas pruebas se corren a mano con make test-e2e.
  • Si querés aislar más, registrá el runner en una VM dedicada en lugar de tu equipo de trabajo.