Protocolo agente–plano de control
Contrato entre el plano de control (cloud-web) y el agente de nodo (cloud-web-node). Los dos viven en
repositorios y se publican por separado, así que este documento es lo que tienen en común.
Versión de protocolo vigente: 1.
Transporte y autenticación
- HTTPS en el puerto del listener de agentes (por defecto
:8443), TLS 1.3. POST /agent/v1/joinsolo exige TLS de servidor: el agente fija la CA por huella (--ca-sha256).- El resto de las rutas exigen certificado de cliente emitido por la CA del clúster para un nodo que siga registrado. Eliminar un nodo revoca su acceso.
- Las órdenes se entregan por una cola persistente con confirmación (
ack); el stream SSE solo avisa de que hay algo nuevo.
Endpoints (protocolo 1)
| Método y ruta | Uso |
|---|---|
POST /agent/v1/join |
Canjea un token de unión; devuelve certificado de cliente, CA y peers |
POST /agent/v1/heartbeat |
Latido y capacidad actual del nodo |
GET /agent/v1/peers |
Peers de la malla WireGuard |
GET /agent/v1/commands |
Órdenes pendientes (deploy, stop) |
POST /agent/v1/commands/{id}/ack |
Confirma una orden |
POST /agent/v1/leave[?force=1] |
El nodo pide su propia baja (desinstalación); 409 si aún corre apps, salvo con force=1: el nodo, que está vivo y es quien se borra, puede forzarla y sus despliegues pasan a fallidos |
POST /agent/v1/report |
Estado y logs de despliegues |
GET /agent/v1/routes |
Tabla de rutas para el ingress |
GET /agent/v1/registry |
Configuración del registro de imágenes interno |
GET /agent/v1/stream |
SSE de avisos |
Los esquemas JSON son los de apps/control-plane/internal/api/agent.go y
apps/node-agent/internal/cpclient/cpclient.go; deben coincidir.
Latido y salud (campo opcional)
POST /agent/v1/heartbeat lleva la capacidad del nodo en el nivel superior (cpu_cores, mem_bytes, disk_bytes) y,
desde la versión de agente 0.2, un campo opcional health:
{"cpu_cores": 8, "mem_bytes": 0, "disk_bytes": 0,
"health": {"registry": {"configured": true, "reachable": false, "error": "i/o timeout"},
"mesh": {"peers": 1, "connected": 0}}}
registry.reachablees si el nodo alcanza el registro de imágenes del clúster; solo se informafalsetras dos chequeos seguidos fallidos.registry.configured=falsesignifica que no hay registro que evaluar.mesh.connectedson los peers con un handshake de los últimos 3 minutos.- Es aditivo: no sube el entero de protocolo. Un servidor anterior ignora el campo y un agente anterior no lo envía; en ese caso la salud del nodo es desconocida y el nodo sigue siendo elegible.
- El plano de control trata una salud de más de 60 s como desconocida y marca un nodo degradado solo si el registro está configurado y el nodo no lo alcanza (la malla se muestra pero no decide). Un nodo degradado no recibe instancias nuevas y se recupera solo cuando vuelve a alcanzar el registro.
Muestras de uso (metrics, campo opcional)
Desde la versión de agente 0.5 el latido puede llevar, una vez cada 30 s (no en cada latido), un campo opcional metrics con el uso real
del nodo y de sus contenedores:
{"cpu_cores": 8, "mem_bytes": 0, "disk_bytes": 0,
"metrics": {"node": {"cpu_pct": 12.5, "mem_used": 8589934592, "disk_used": 128849018880},
"containers": [{"container": "cw-web-3", "cpu_cores": 0.25, "mem_bytes": 104857600, "mem_limit": 536870912}],
"omitted": 0}}
node.cpu_pctes el porcentaje del total de núcleos (0 a 100) entre dos lecturas de/proc/stat;mem_usedesMemTotal - MemAvailable;disk_usedsale destatfsdel directorio de datos. Lo que no se pudo medir falta (p. ej.cpu_pcten la primera muestra de un arranque): «sin dato» no es cero.containersson los contenedores de apps (cw-*) con su CPU en núcleos usados (Docker lo da relativo a un núcleo y puede pasar de 100 %), su memoria y su límite. Una sola llamada adocker stats --no-streampor muestra, con tiempo límite de 10 s y en una goroutine aparte: ni su duración (~2,5 s con unos pocos contenedores) ni un fallo retrasan o impiden el latido. Sin contenedores en ejecución no se llama a Docker.- Cada contenedor puede traer
restarts(reinicios) ystarted_at(arranque de la corrida actual, RFC 3339), de una llamadadocker inspectpor muestra (como mucho dos llamadas a Docker en total, con el mismo tiempo límite); si falla, el contenedor sale sin esos campos. - Como máximo 100 contenedores por muestra (los de más memoria);
omittedcuenta los que quedaron afuera. Con 100 contenedores el campo pesa ~10 KB. - Es aditivo: no sube el entero de protocolo. Un servidor anterior ignora el campo; un agente anterior no lo envía y el panel de monitoreo muestra «sin datos» para ese nodo.
Tráfico HTTP por host (metrics.traffic, campo opcional)
Un nodo con ingress (capacidad traffic-metrics) suma a la muestra de uso el tráfico de las últimas 30 s por host:
{"metrics": {"traffic": [{"host": "web.zondacloud.com", "requests": 120, "r2xx": 110, "r3xx": 4, "r4xx": 5, "r5xx": 1, "dur_sum_ms": 5400.5, "slow": 2}],
"traffic_omitted": 0}}
- Son deltas del intervalo (no acumulados).
slowson los pedidos de más de 1 s; la latencia media esdur_sum_ms / requests. - Caddy escribe su registro de accesos en un socket local del agente; el agente decodifica solo host, código y duración y cuenta únicamente los hosts de la tabla de rutas (un Host cualquiera no cuenta). Rutas, parámetros, direcciones de cliente y cabeceras nunca se leen ni se mandan.
- Hasta 200 hosts por muestra (los de más pedidos);
traffic_omittedcuenta los que quedaron afuera. Aditivo: un servidor anterior lo ignora. - Si el agente se reinicia, los pedidos de ese hueco no se cuentan: es una estimación, no un registro contable.
Capacidades del agente (health.features)
Desde la versión de agente 0.3 el mismo campo health puede llevar features, la lista de capacidades opcionales de ese agente:
{"health": {"registry": {...}, "mesh": {...}, "features": ["static-sites"]}}
static-sites: sabe construir y servir un sitio estático a partir de los ajustes de build de la orden de despliegue.traffic-metrics: un nodo de ingress que informa el tráfico HTTP por host (metrics.traffic).- Es aditivo, igual que
health: no sube el entero de protocolo. Un agente anterior no lo envía y un plano de control anterior lo ignora. - Como la salud caduca a los 60 s, una capacidad sin salud vigente no se supone.
- El plano de control solo exige una capacidad cuando la app lo necesita (ver abajo); para todo lo demás un agente sin
featuressigue siendo un nodo elegible.
Docker Desktop
Un nodo cuyo motor de Docker es Docker Desktop informa registry.configured=true, reachable=false con el motivo
Docker Desktop is not supported on a node…: el chequeo del registro lo hace el agente desde su red, pero quien sube las
imágenes es el daemon, que en Docker Desktop corre en otra VM y no ve la malla. El nodo queda degradado y no recibe despliegues.
Orden de despliegue y sitios estáticos (build)
La orden deploy puede llevar un bloque opcional build con los ajustes de build que el usuario fijó para un sitio estático:
{"deployment_id": "dep_…", "source": {…}, "env": {…}, "port": 0,
"build": {"install_cmd": "npm ci", "build_cmd": "npm run build:prod", "output_dir": "dist", "spa": "yes"}}
- Todos los campos son opcionales;
spaesauto,yesono. Sin el bloque, el agente detecta todo. - Solo se envía en despliegues desde el código (no en un rollback ni en un reprogramado, que reutilizan una imagen ya construida).
- El plano de control valida los valores y el agente vuelve a validarlos (largo, sin saltos de línea, directorio relativo y dentro del proyecto): no confía en el servidor.
- Un agente anterior ignora el bloque. Para que no construya por buildpacks algo que no puede construirse, el planificador solo envía
una app con ajustes explícitos a nodos que informan
static-sites; si no hay ninguno, el despliegue falla diciendo qué nodos hay que actualizar. Una app sin ajustes se envía a cualquier nodo y el agente decide la estrategia (Dockerfile, sitio estático o buildpacks). - Solo las variables de la app con prefijo público (
VITE_,REACT_APP_,NEXT_PUBLIC_,PUBLIC_,GATSBY_) entran al build de un sitio estático; el resto no.
Versionado
Cada petición del agente lleva dos cabeceras:
| Cabecera | Valor |
|---|---|
X-Cloudweb-Agent-Version |
versión del binario (p. ej. 0.1.0) |
X-Cloudweb-Protocol |
entero de protocolo que habla el agente |
- El entero de protocolo solo sube ante cambios incompatibles (quitar o cambiar el significado de un campo, de una ruta o de un flujo). Agregar campos opcionales o rutas nuevas no lo sube.
- Un agente que no envía la cabecera es anterior al versionado y se trata como protocolo
1. - El plano de control declara el rango que entiende (
MinAgentProtocol–MaxAgentProtocol).
Incompatibilidad
Si el protocolo del agente queda fuera del rango, el plano de control responde antes de ejecutar cualquier acción (no cambia estado, no consume el token de unión):
HTTP/1.1 426 Upgrade Required
{
"error": "protocol 2 is not supported: this control plane supports protocols 1 to 1; upgrade the control-plane",
"agent_version": "0.3.0",
"agent_protocol": 2,
"supported_min": 1,
"supported_max": 1,
"upgrade": "control-plane"
}
upgrade indica qué lado debe actualizarse (agent si el agente es más viejo que el mínimo,
control-plane si es más nuevo que el máximo). Una cabecera mal formada da 400.
El agente, al recibir 426, lo registra como error con el rango soportado y baja el ritmo de
reintento a uno cada 5 minutos; vuelve al ritmo normal en cuanto el plano de control lo acepta.
Cómo evolucionar el protocolo
- Cambios compatibles: se hacen en cualquier lado, sin subir el entero.
- Cambios incompatibles: subir
MaxAgentProtocoldel servidor para que acepte ambos protocolos durante una ventana de transición, publicar el servidor, y recién después el agente que habla el nuevo. Retirar el protocolo viejo (MinAgentProtocol) en una versión posterior, avisándolo en las notas. - Cada repositorio prueba su pipeline contra la última versión publicada del otro (ver
tests/e2e/y el pipeline decloud-web-node).