- reports/0001-2026-06-07-unibus-grafana-monitoring.md - reports/0008-2026-06-07-unibus-admin-users-wired.md - reports/0008-2026-06-07-unibus-decentralization-audit.md - reports/0009-2026-06-07-unibus-cluster-hardening.md - reports/0010-2026-06-07-unibus-android-native.md - reports/0011-2026-06-07-unibus-cluster-deploy.md - reports/0012-2026-06-07-unibus-deploy-gaps-closed.md - reports/0013-2026-06-07-unibus-admin-panel.md - reports/0014-2026-06-07-unibus-users-http-admin-api.md - reports/0015-2026-06-07-unibus-web-wired.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.6 KiB
Report 0017 — Rollout consolidado unibus 0.11.0 al cluster de 3 nodos (ROLLING, gate R3)
- Fecha: 07/06/2026
- Autor: agente (Claude Code)
- Ámbito:
apps/unibus(membershipd) +apps/unibus_admin(panel) — cluster HA producción magnus + homer + datardos - Estado: done — los 3 nodos en 0.11.0, R3 pleno, panel redeployado, sin rollbacks
Resumen
Rollout rolling del binario membershipd 0.11.0 a los tres nodos del cluster, uno a uno
con gate de reconvergencia R3 entre cada nodo, activando en el mismo ciclo de restart el
monitoring NATS loopback limpio (UNIBUS_NATS_MONITOR=1, desacoplado del debug-log). Un
solo ciclo de restart por nodo desbloqueó las tres cosas previstas: la API /users (panel
admin, antes 404 con request firmado), las métricas NATS en 127.0.0.1:8222, y el cierre
del gap del binario viejo (report 0012). Después, re-deploy del panel admin en magnus. El
cluster mantuvo quórum (2/3) en todo momento; cada nodo reconvergió a R3 (6/6 streams KV a
followers_current=2/2) antes de tocar el siguiente. No fue necesario ningún rollback.
Cambios
| Fase | Acción | Resultado |
|---|---|---|
| 0 — Integrar | git merge --no-ff quick/nats-monitor-flag → master (commit f31580d), app.md → 0.11.0 |
Sin conflictos; web/ no tocado |
| 0 — Verificar | go build ./... + tests membership/client/embeddednats/cmd/membershipd |
Todo verde; push a origin/master |
| 1 — Build | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /tmp/membershipd_0.11.0 ./cmd/membershipd |
ELF static x86-64, sha f6096a8e…, --help OK |
| 2 — Rollout | magnus → homer → datardos: backup + drop-in UNIBUS_NATS_MONITOR=1 + binario + restart + gate R3 |
3/3 reconvergidos, sin perder quórum |
| 3 — Verificar | healthz 3/3, /users firmado 200 3/3, 8222 3/3, sin debug, R3 6/6 a 2/2 |
Todo verde |
| 4 — Panel | rebuild master (Users cableado) + backup + deploy magnus + restart unibus-admin.service |
/api/users 200, end-to-end vía Caddy 200 |
Detalle del flag (0.11.0): función pura natsLogOpts(debugEnv, monitorEnv) (noLog, debug, trace, monitor)
con monitor = monitorEnv=="1" || debug y noLog = !debug. Con MONITOR=1 el endpoint de
monitoring abre en 127.0.0.1:8222 (bind loopback hardcoded) dejando el log del nats-server
en silencio (NoLog true). El acoplamiento inverso se mantiene (DEBUG implica MONITOR).
Drop-in systemd aditivo: /etc/systemd/system/membershipd-cluster.service.d/nats-monitor.conf
con [Service] + Environment=UNIBUS_NATS_MONITOR=1 — no toca el unit base ni cluster.env.
El binario nuevo es drop-in del unit existente: se verificó que acepta los 17 flags del
ExecStart (incluido --internal-id-file, presente solo en el unit de datardos). No se
modificó ningún unit ni cluster.env.
Verificación
Unit y baseline (confirmados, no asumidos)
ExecStart=/opt/unibus/membershipd … (EnvironmentFile=/opt/unibus/cluster.env)
HTTP_PORT=8470 NATS_CLIENT_PORT=4250
healthz baseline (3 nodos): {"posture":{"enforce":true,"acl":true,"tls":true,"cluster":true,"store":"kv"},"status":"ok"}
8222 baseline: CERRADO en los 3 (esperado, sin drop-in)
binario viejo: magnus/homer 29337645 b; datardos 29374815 b (0012, con --internal-id-file)
Hallazgo clave del gate R3 — métrica autoritativa
La métrica followers_current de /jsz solo es autoritativa consultada en el LEADER del
stream. Un nodo follower (recién reiniciado) reporta su vista desactualizada de los otros
peers (current=false, lag constante) durante minutos sin que sea un problema real. La
señal autoritativa por nodo es el /healthz del nats-server (127.0.0.1:8222/healthz):
devuelve 503 si cualquier stream del que el nodo es miembro está lagging, 200 sólo cuando
todos están current. Se usó ese endpoint como gate primario por nodo, y la vista del
stream-leader para el conteo explícito 2/2.
Gate R3 por nodo (orden magnus → homer → datardos)
| Nodo | Backup | membershipd healthz | nats /healthz | meta (leader/size/pending) | 8222 /varz | [DBG]/[TRC] | quórum otros 2 |
|---|---|---|---|---|---|---|---|
| magnus | membershipd.bak.20260607-192416Z |
200 enforce+acl+tls+cluster+kv | 200 ok | homer / 3 / 0 | 200 | 0 | homer 200, dd 200 |
| homer | membershipd.bak.20260607-193313Z |
200 idem | 200 ok (t=0) | datardos / 3 / 0 | 200 | 0 | magnus 200, dd 200 |
| datardos | membershipd.bak.20260607-193452Z |
200 idem | 200 ok (t=0) | homer / 3 / 0 | 200 | 0 | magnus 200, homer 200 |
Re-elecciones de meta-leader observadas (esperadas, quórum 2/3 intacto): inicial homer →
restart homer → datardos → restart datardos → homer (final). Tras completar los tres, la
vista del stream-leader (magnus) confirmó los 6 buckets KV
(users/rooms/members/room_keys/rooms_by_member/nonces) a followers_current=2/2:
streams KV a 2/2: 6/6 (stream-leader=magnus); meta-leader=homer cluster_size=3 pending=0
FASE 3 — verificación post-rollout (3 nodos)
magnus homer datardos
membershipd 200 200 200
posture enforce+acl+tls+cluster+store=kv (homogénea)
/users firmado 200 200 200 (3 users idénticos — KV replicado R3)
8222 varz/jsz/connz 200/200/200 200/200/200 200/200/200
nats /healthz ok ok ok
journal DBG/TRC 0 0 0
/users firmado se verificó con un verificador throwaway (client.Connect + ListUsers
del propio pkg/client, identidad operator desde pass unibus/operator-identity en archivo
local 0600, TLS+nkey contra cada nodo por su IP pública; SAN del cert cubre las IPs). Antes
del rollout, un GET /users firmado daba 404 (ruta ausente en el binario viejo); ahora da
200 con la allowlist:
operator role=admin status=active
gapcheck_user role=member status=revoked (residuo E2E del report 0012)
gapcheck_user2 role=member status=revoked (residuo E2E del report 0012)
El verificador y la copia local de la identidad se eliminaron (cmd/usercheck borrado del
working tree; /tmp/operator.id con shred). Working tree de unibus limpio
(master...origin/master).
FASE 4 — deploy del panel admin
panel binario nuevo: /opt/unibus_admin/unibus_admin (sha 812831…), backup unibus_admin.bak.20260607-194114Z
service unibus-admin.service: active; journal limpio ("users backend: control-plane", "bus TLS+nkey: ON", "cluster nodes probed: 3")
GET /api/users (loopback 8480): HTTP 200 — lista real (3 users), ya no 502
GET /api/cluster: 3 nodos up:true, posture homogénea, latencias 0/10/5 ms
end-to-end vía Caddy (basic auth admin): GET https://admin-…/api/users -> HTTP 200 con lista
end-to-end sin auth: HTTP 401 (Caddy basic_auth protege)
El binario del panel se reconstruyó desde master (frontend web/dist ya embebido, no se tocó
ningún web/). El Caddyfile no se modificó: el site (reverse_proxy 127.0.0.1:8480 +
basic_auth) quedó intacto; no hizo falta caddy reload (mismo puerto/config). La cadena
completa navegador → Caddy(TLS+auth) → panel → bus firmado /users → KV R3 está probada.
Seguridad
- Secretos siempre desde
pass/path, nunca en git ni argv: identidad operator a archivo local 0600 (borrada conshred); password del panel pasado a curl por stdin (-K -), nunca en la línea de comandos. - Monitoring vía
UNIBUS_NATS_MONITOR(drop-in), nuncaUNIBUS_NATS_DEBUG: confirmado 0 líneas[DBG]/[TRC]en journald de los 3 nodos. 8222 bind loopback hardcoded. - Backups reversibles por nodo conservados en
/opt/unibus/(membershipd) y/opt/unibus_admin/(panel) — listados arriba. - Gate R3 estricto, un nodo abajo a la vez; quórum 2/3 nunca perdido. magnus (host crítico):
sólo se reinició
membershipd-cluster.service. Posture homogénea mantenida.
Gaps / pendientes
- Verificación visual de la pestaña Users con navegador: no realizada. Se verificó la
cadena funcionalmente end-to-end (Caddy basic_auth → panel → bus →
/api/users200 con la lista real). No se abrió el navegador para no exponer eladmin-panel-passworden la URL (el basic_auth de Caddy obliga a meter las credenciales en la URL o en un dialog nativo no automatable de forma segura). El render de la SPA es determinista sobre/api/users, que responde 200. - Los usuarios
gapcheck_user/gapcheck_user2(revoked) son residuos de los checks E2E del report 0012, no usuarios reales; pueden limpiarse del KV si se desea (no bloquea nada). - Binarios temporales en
/tmp(locales y de los nodos) eliminados. Los backups.bak.*se conservan a propósito para reversibilidad.