d43ffae3ae
- 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>
148 lines
8.6 KiB
Markdown
148 lines
8.6 KiB
Markdown
# 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 con `shred`); password del panel pasado a curl por stdin (`-K -`),
|
|
nunca en la línea de comandos.
|
|
- Monitoring vía `UNIBUS_NATS_MONITOR` (drop-in), **nunca** `UNIBUS_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/users` 200 con la
|
|
lista real). No se abrió el navegador para no exponer el `admin-panel-password` en 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.
|