Skip to content

TLSStress.Art — Deployment Tri-Node en 3 Servidores UCS

Modo de deployment: tri-node — UCS-1 ejecuta únicamente agentes browser engine, UCS-2 ejecuta únicamente agentes synthetic-load engine, y UCS-3 ejecuta personas + servicios + observabilidad. Para una configuración de un solo servidor véase UBUNTU_K3S_SINGLENODE_QUICKSTART_DEPLOY.es.md; para la topología de dos servidores véase UBUNTU_K3S_DUALNODE_QUICKSTART_DEPLOY.es.md; para el modo distribuido en cuatro servidores véase UBUNTU_K3S_MULTINODE_QUICKSTART_DEPLOY.es.md.

Objetivo: Desplegar un TLSStress.Art a través de exactamente tres servidores Cisco UCS — uno dedicado a la generación de carga con browser engine, otro dedicado a la generación de carga con synthetic-load engine, y un tercero dedicado a webservers + servicios + observabilidad — para ofrecer un punto de escala intermedio entre dual-node y multi-node de cuatro servidores, y aislar los dos runtimes de agentes entre sí.

Última verificación contra el código de producción: v3.7.0 (2026-05-12) — Ver ARCHITECTURE.md para los 37 MÓDULOs canónicos + 7 Test Kinds + arquitectura de safety DOM/CPOS/PIE-PA + la postura ZTP-prem 12/12 capas contra operador insider (25 reivindicaciones de patente, partición Tier A/B, Confidential Computing, audit log sellado en hash-chain, webhook de admission K8s, TPM 2.0 measured-boot, monitor DLP de egress, detector de anomalías comportamental). ADRs 0014, 0019-0025 cubren adiciones post-Freeze. Para quién es esta guía: Ingenieros de redes y sistemas que operan un test bed de NGFW y disponen de tres chasis UCS, y desean aislamiento de hardware entre agentes browser engine, agentes synthetic-load engine y webservers sin levantar un cuarto chasis para los servicios.

Tiempo estimado: 75–105 minutos para la configuración inicial del cluster; 15 minutos para teardown + redeploy.


Instalación automatizada (recomendada)

Ejecuta el script de instalación una vez por servidor en este orden exacto (UCS-3 primero porque hospeda el control plane de k3s):

# Clonar el repositorio en cada servidor
git clone https://github.com/nollagluiz/AI_forSE.git && cd AI_forSE

# UCS-3 — k3s server + personas + servicios + observabilidad
sudo ./scripts/k8s-install.sh --mode=tri-server --data-iface=eth1
# ↑ Imprime JOIN_TOKEN y SERVER_IP al final — guárdalos.

# UCS-1 — solo agentes browser-engine
sudo ./scripts/k8s-install.sh --mode=tri-agent --role=playwright \
     --server-ip=<UCS-3-OOBI-IP> --token=<JOIN_TOKEN> --data-iface=eth1

# UCS-2 — solo agentes synthetic-load
sudo ./scripts/k8s-install.sh --mode=tri-agent --role=k6 \
     --server-ip=<UCS-3-OOBI-IP> --token=<JOIN_TOKEN> --data-iface=eth1

# De vuelta en UCS-3 — aplicar todos los manifiestos de Kubernetes
sudo ./scripts/k8s-install.sh --mode=tri-apply

Dry run: agrega --dry-run a cualquier comando para previsualizar sin aplicar cambios.

El resto de esta guía explica cada paso en detalle y resulta útil para personalizar la instalación o diagnosticar problemas en la instalación automatizada.


Tabla de contenidos

  1. Por qué tri-node
  2. Visión general de la arquitectura
  3. Red OOBI — obligatoria en los tres UCS
  4. Prerrequisitos por servidor
  5. Paso 1 — Preparación del host (los tres UCS)
  6. Paso 2 — Bootstrap del cluster k3s
  7. Paso 3 — Configuración de VLAN por nodo
  8. Paso 4 — Aplicar manifiestos
  9. Paso 5 — Verificar el deployment
  10. Observabilidad — Grafana y Prometheus en tri-node
  11. Diagnóstico de problemas
  12. Referencia — archivo overlay

1. Por qué tri-node

Elige tri-node cuando:

  • Dispones de tres chasis UCS y deseas aislamiento de hardware entre agentes browser engine, agentes synthetic-load engine y webservers.
  • Los dos runtimes de agentes no deben competir por recursos. browser engine lanza procesos Chromium headless (intensivos en memoria e IO); synthetic-load engine ejecuta un único proceso Go que emite decenas de miles de peticiones HTTP/2 + HTTP/3 (intensivo en CPU y conexiones). Compartir un mismo host significa que cualquiera de los runtimes que pique primero hará throttle del otro, y tus mediciones del NGFW se vuelven ruidosas.
  • No tienes un cuarto chasis para la capa de servicios (ese sería el layout multi-node).

Comparado con los demás modos:

Single Dual Tri Multi
Cantidad de UCS 1 2 3 4
browser-engine + synthetic-load comparten host sí (ambos en UCS-1) no — UCS separados no
Servicios en el host de los agentes no no no
Aislamiento de hardware entre browser engine y synthetic-load engine
Aislamiento de hardware entre agentes y webservers
Chasis dedicado para servicios

2. Visión general de la arquitectura

Topología física

            ┌────────────────────────────────────────────────────────┐
            │      Red OOBI — eth0 en LOS TRES UCS                    │
            │              10.0.0.0/24 (ejemplo)                      │
            │   API k3s :6443 · kubelet · scrape de Prometheus        │
            └──────┬───────────────────┬────────────────┬─────────────┘
                   │                   │                │
          ┌────────┴───────┐  ┌────────┴────────┐  ┌────┴───────────────────┐
          │     UCS-1        │  │     UCS-2        │  │            UCS-3        │
          │  role=playwright │  │   role=k6        │  │       role=ngfw-dut     │
          │                  │  │                  │  │                          │
          │   Solo agentes   │  │   Solo agentes   │  │   20 pods Caddy persona │
          │   browser engine     │  │   synthetic-load engine             │  │   10 slots cloned-      │
          │                  │  │                  │  │   persona               │
          │                  │  │                  │  │   Dashboard · Postgres  │
          │                  │  │                  │  │   PgBouncer · Cloner    │
          │                  │  │                  │  │   NFS · Grafana         │
          │                  │  │                  │  │   Prometheus            │
          │                  │  │                  │  │   SNMP exporter          │
          │  eth0 (OOBI)     │  │  eth0 (OOBI)     │  │   eth0 (OOBI)           │
          │  eth1 (trunk)    │  │  eth1 (trunk)    │  │   eth1 (trunk)          │
          └────────┬─────────┘  └────────┬─────────┘  └──────────┬──────────────┘
                   │                     │                       │
            VLAN 20 (172.16/16)   VLAN 30 (172.17/16)   VLAN 99 mgmt (192.168.90/24)
                                                          VLAN 40 ISP (DHCP via macvlan)
                                                          VLANs 101–120 (10.1.x/27)
                                                          VLANs 200–209 (10.2.x/27)
                   │                     │                       │
                   └─────────┬───────────┴───────────────┬───────┘
                             │                           │
                  ┌──────────┴──────────────┐
                  │   Cisco Nexus 9000      │
                  │   VLAN trunk · ECMP     │
                  │   DSCP AF41 · MTU 9216  │
                  └──────────┬──────────────┘
                             │
                  ┌──────────┴──────────────┐
                  │  NGFW (Device Under Test)│
                  │  TLS leg-1: agentes→NGFW│
                  │  TLS leg-2: NGFW→Caddy  │
                  └─────────────────────────┘

Distribución de cargas

Carga UCS-1 (role=playwright) UCS-2 (role=k6) UCS-3 (role=ngfw-dut)
Agentes browser engine (web-agent)
Agentes synthetic-load engine (k6-agent)
Personas sintéticas (20× Caddy)
Slots de cloned-persona (10× Caddy)
Dashboard, Postgres, PgBouncer
Cloner
Servidor NFS (cloned-sites)
SNMP exporter
Prometheus + Grafana
node_exporter (métricas por host) ✓ DaemonSet ✓ DaemonSet ✓ DaemonSet
node-tuning (sysctl + BBR + CPU governor) ✓ DaemonSet (dut-data-plane=true) ✓ DaemonSet (dut-data-plane=true) ✓ DaemonSet (dut-data-plane=true)
cni-dhcp (DHCP del ISP del cloner) ✓ DaemonSet

Por qué role=ngfw-dut en UCS-3 (en lugar de role=infra)

Las personas sintéticas (personas/_generated/*/deployment.yaml), las cloned-personas (k8s/clone-personas/20-slots.yaml) y el SNMP exporter (k8s/dut/60-snmp-exporter.yaml) tienen todos hardcoded nodeSelector: role=ngfw-dut. Reutilizar esa label en UCS-3 evita tocar más de veinte manifiestos. El overlay tri-node entonces parcha las cargas adicionales de servicios (Dashboard, Postgres, PgBouncer, Cloner) — que el overlay multi-node envía a role=infra — al mismo nodo role=ngfw-dut. Es exactamente el mismo esquema que utiliza el overlay dual-node para UCS-2.


3. Red OOBI — obligatoria en los tres UCS

La red OOBI (out-of-band infrastructure) es el segmento dedicado del control plane que circula sobre eth0 en cada UCS, sin importar el modo de deployment.

Qué corre sobre OOBI

  • API server de k3s (:6443) — UCS-1 y UCS-2 lo alcanzan en UCS-3 para unirse al cluster
  • kubelet (:10250) — UCS-3 hace scrape de UCS-1 y UCS-2 para obtener el estado de los pods
  • flannel (CNI) — el tráfico pod-a-pod entre nodos usa VXLAN sobre eth0
  • Scrape de Prometheusnode_exporter, cAdvisor, kube-state-metrics se alcanzan todos por eth0
  • Renovaciones de cert-manager — webhook + tráfico de challenge ACME
  • Dashboard / kubectl — acceso del operador

Qué NO corre sobre OOBI

  • Tráfico de prueba al NGFW — eso va por el VLAN trunk de eth1 (data plane)
  • Polling SNMP — VLAN 99 (192.168.90.0/24) en eth1.99
  • Salida a internet del cloner — VLAN 40 en eth1.40

Requisitos por UCS

UCS-1 (browser engine) UCS-2 (synthetic-load engine) UCS-3 (servicios)
IP de eth0 estática, subnet OOBI estática, subnet OOBI estática, subnet OOBI
Alcanzable desde el eth0 del par
MTU 1500 (default) 1500 (default) 1500 (default)
Ruta default vía OOBI ✓ — salida a internet para image pulls e instalación de k3s

Si eth0 falta en cualquier UCS, flannel de k3s no logra establecer el control plane, el nodo de agente afectado no consigue unirse al cluster, y Prometheus en UCS-3 no puede hacer scrape de las métricas de host del nodo faltante. La verificación pre-flight en k8s-install.sh rechaza los tres modos (tri-server, tri-agent y la validación por nodo en tri-apply) si eth0 está ausente.


4. Prerrequisitos por servidor

Hardware

UCS-1 (browser engine) UCS-2 (synthetic-load engine) UCS-3 (servicios)
CPU ≥ 16 cores físicos ≥ 16 cores físicos ≥ 32 cores físicos
RAM ≥ 64 GB ≥ 32 GB ≥ 128 GB
NICs eth0 (OOBI) + eth1 (trunk) eth0 (OOBI) + eth1 (trunk) eth0 (OOBI) + eth1 (trunk)
Disco ≥ 100 GB libres en / ≥ 100 GB libres en / ≥ 500 GB libres en / (NFS + Postgres + retención de Prometheus)

UCS-3 es el nodo más cargado. browser engine (UCS-1) suele ser más pesado que synthetic-load engine (UCS-2) porque cada agente lanza su propio Chromium headless.

Sistema operativo

  • Ubuntu 22.04 LTS o 24.04 LTS, amd64
  • acceso root (o sudo)
  • acceso a internet vía ruta default OOBI durante la instalación (k3s + Helm + imágenes de contenedor)

Red — Nexus 9000

VLANs que deben estar configuradas en el trunk hacia los tres UCS:

VLAN UCS-1 UCS-2 UCS-3 Subnet Uso
20 172.16.0.0/16 Agentes browser engine
30 172.17.0.0/16 Agentes synthetic-load engine
40 DHCP del ISP Salida a internet del cloner (macvlan)
99 192.168.90.0/24 Polling SNMP — Nexus (.2), NGFW (.3)
101–120 10.1.{1..20}.0/27 Webservers de personas sintéticas
200–209 10.2.{1..10}.0/27 Slots de personas clonadas

Estrictamente: la VLAN 20 debe llegar solo a UCS-1; la VLAN 30 solo a UCS-2; las VLANs 40, 99, 101–120 y 200–209 solo a UCS-3. El trunk Nexus puede transportarlas todas y dejar que cada lado ignore las que no consume — el trunk es idéntico para los tres puertos.


Paso 1 — Preparación del host (los tres UCS)

Preparación idéntica en UCS-1, UCS-2 y UCS-3:

sudo apt-get update
sudo apt-get install -y --no-install-recommends \
  curl git jq openssl ca-certificates gnupg lsb-release \
  vlan iproute2 ethtool net-tools

# Verificar ambas NICs
ip link show eth0
ip link show eth1

Después ejecuta el script de host-tuning del proyecto en cada UCS — sysctls para QUIC + TCP de alto fan-out, BBR + FQ, CPU governor, conntrack, puertos:

sudo scripts/host-tuning.sh apply
sudo scripts/host-tuning.sh status

El paso apply es idempotente y persiste vía drop-ins de systemd-sysctl. En cada corrida de medición se disparará la alerta TestBedSysctlMissing del dashboard si algún UCS regresa a los defaults del kernel.


Paso 2 — Bootstrap del cluster k3s

UCS-3 primero (es el k3s server)

sudo ./scripts/k8s-install.sh --mode=tri-server --data-iface=eth1

El script:

  1. Hace pre-flight de eth0 + eth1 + interfaz del ISP
  2. Crea las subinterfaces VLAN 40, 99, 101–120, 200–209 sobre eth1
  3. Levanta la subinterfaz ISP eth1.40 y la persiste vía netplan
  4. Instala el k3s server, asocia la API a eth0, deshabilita Traefik y ServiceLB
  5. Instala Helm, cert-manager, Multus
  6. Etiqueta el nodo role=ngfw-dut + dut-data-plane=true
  7. Imprime el JOIN_TOKEN y SERVER_IP

Guarda las credenciales impresas — UCS-1 y UCS-2 las necesitan.

UCS-1 (browser engine)

sudo ./scripts/k8s-install.sh --mode=tri-agent --role=playwright \
     --server-ip=<UCS-3-OOBI-IP> --token=<JOIN_TOKEN> --data-iface=eth1

El script:

  1. Hace pre-flight de eth0 + eth1
  2. Crea la subinterfaz VLAN 20 sobre eth1 (172.16.0.1/16)
  3. Une al cluster k3s como agent vía eth0 → UCS-3:6443
  4. Se etiqueta a sí mismo role=playwright + dut-data-plane=true

UCS-2 (synthetic-load engine)

sudo ./scripts/k8s-install.sh --mode=tri-agent --role=k6 \
     --server-ip=<UCS-3-OOBI-IP> --token=<JOIN_TOKEN> --data-iface=eth1

Idéntico a UCS-1 pero crea VLAN 30 (172.17.0.1/16) y se etiqueta a sí mismo role=k6.

Verificar que los tres nodos se unieron

Desde UCS-3:

kubectl get nodes -o wide
# Esperado:
# NAME    STATUS  ROLES    AGE  VERSION  LABELS
# ucs-1   Ready   agent    2m   v1.31.x  role=playwright,dut-data-plane=true,...
# ucs-2   Ready   agent    1m   v1.31.x  role=k6,dut-data-plane=true,...
# ucs-3   Ready   master   5m   v1.31.x  role=ngfw-dut,dut-data-plane=true,...

Paso 3 — Configuración de VLAN por nodo

El script de instalación lo realiza automáticamente. Para verificar:

UCS-1 (browser engine):

ip -br link | grep eth1\\.
# eth1.20  UP  (172.16.0.1/16)

UCS-2 (synthetic-load engine):

ip -br link | grep eth1\\.
# eth1.30  UP  (172.17.0.1/16)

UCS-3 (servicios):

ip -br link | grep eth1\\.
# eth1.40  UP  (sin IP — DHCP vía macvlan del pod cloner)
# eth1.99  UP  (192.168.90.1/24)
# eth1.101 UP  (10.1.1.1/27)
# … hasta eth1.120 (10.1.20.1/27)
# eth1.200 UP  (10.2.1.1/27)
# … hasta eth1.209 (10.2.10.1/27)

La persistencia de las VLANs al reiniciar la maneja netplan (el script escribe /etc/netplan/99-*.yaml).


Paso 4 — Aplicar manifiestos

Desde UCS-3:

# Crear el configmap con la CA del NGFW (una sola vez)
kubectl create configmap ngfw-ca -n web-agents --from-file=ngfw-ca.crt=<path-al-cert>

# Crear los secrets de la aplicación a partir de .env
kubectl create secret generic web-agent-secrets -n web-agents --from-env-file=.env

# Aplicar todo
sudo ./scripts/k8s-install.sh --mode=tri-apply

Esto aplica:

  1. kubectl apply -k overlays/tri-node/ — manifiestos base de k8s con browser engine fijado a role=playwright, synthetic-load engine fijado a role=k6, y Dashboard/Postgres/PgBouncer/Cloner fijados a role=ngfw-dut
  2. kubectl apply -k k8s/dut/ — overlay DUT (NADs, sondas SNMP, servidor NFS, DaemonSet node-tuning, ServiceMonitor de cAdvisor, reglas de Prometheus de infra)
  3. Parcha el nodeSelector del DaemonSet node-tuning a dut-data-plane=true (corre en los tres UCS)
  4. Parcha los deployments browser-engine + synthetic-load vía 40-playwright-patch.yaml / 50-k6-patch.yaml — anotación de macvlan Multus net1, trust de la CA del NGFW, REJECT_INVALID_CERTS=true
  5. Re-fija browser engine a role=playwright y synthetic-load engine a role=k6 (los archivos de patch usan role=ngfw-dut por defecto para compatibilidad single-node — tri-node hace override por runtime)
  6. Espera a que todos los pods queden Ready

Paso 5 — Verificar el deployment

# Pods en UCS-1 (browser engine)
kubectl get pods -n web-agents -o wide --field-selector spec.nodeName=ucs-1
# Esperado: solo web-agent-*

# Pods en UCS-2 (synthetic-load engine)
kubectl get pods -n web-agents -o wide --field-selector spec.nodeName=ucs-2
# Esperado: solo k6-agent-*

# Pods en UCS-3 (servicios + personas)
kubectl get pods -n web-agents -o wide --field-selector spec.nodeName=ucs-3
# Esperado: dashboard, postgres, pgbouncer, cloner, nfs-server,
#           persona-shop-*, persona-news-*, …, clone-persona-1-*, …

# Personas sintéticas + clonadas en sus propios namespaces
kubectl get pods --all-namespaces -o wide | grep -E "persona-|clone-persona-"

# Multus net1 anexado a browser engine
kubectl exec -n web-agents deploy/web-agent -- ip -br addr | grep net1
# Esperado: una dirección 172.16.x.x

# Multus net1 anexado a synthetic-load engine
kubectl exec -n web-agents deploy/k6-agent -- ip -br addr | grep net1
# Esperado: una dirección 172.17.x.x

# Smoke test end-to-end — browser engine → NGFW → persona
kubectl exec -n web-agents deploy/web-agent -- \
  curl -sf https://shop.persona.local/ | head -1

6. Observabilidad — Grafana y Prometheus en tri-node

La observabilidad corre íntegramente en UCS-3 (role=ngfw-dut):

  • Prometheus — hace scrape de los tres UCS sobre OOBI (eth0) en el puerto :9100 para node_exporter, kubelet :10250 para cAdvisor y :8080 para kube-state-metrics
  • Grafana — el dashboard Test-Bed Infrastructure Health se adapta automáticamente a la topología de tres nodos vía filtros por host y reglas de recording count(node_exporter)
  • Alertas — la alerta compuesta TestBedInfrastructureBottleneck, las de nivel host (HostUDPBufferOverflow, HostConntrackNearFull, etc.) y las de nivel pod (PodCPUThrottled, OOMKilled) funcionan todas out-of-the-box. Véase MONITORING_TEST_VALIDITY.es.md para el catálogo completo de alertas

El DaemonSet node_exporter no tiene nodeSelector y trae tolerations: operator: Exists, por lo que cubre los tres UCS sin configuración adicional. La alerta NodeExporterCoverageIncomplete se dispara automáticamente si algún nodo deja de reportar métricas de host.

En tri-node específicamente, los paneles de Grafana separan de forma natural las métricas de host de browser engine (UCS-1) de las de synthetic-load engine (UCS-2) gracias a la dimensión de la label node — puedes saber de un vistazo si un runtime es el cuello de botella mientras el otro está ocioso, algo que un dual-node con un único host de agentes no permite ver.

Para abrir Grafana desde tu workstation de operador (asumiendo que tienes acceso kubectl a UCS-3):

kubectl port-forward -n web-agents svc/grafana 3000:3000
# luego visita http://localhost:3000

7. Diagnóstico de problemas

UCS-1 o UCS-2 falla al unirse al cluster

Síntomas: la instalación tri-agent se cuelga en Joining k3s cluster as agent.

# En el UCS que falla — verificar alcance OOBI a UCS-3
ping <UCS-3-OOBI-IP>
nc -vz <UCS-3-OOBI-IP> 6443

# En el UCS que falla — verificar el estado y logs del servicio k3s-agent
sudo systemctl status k3s-agent
sudo journalctl -u k3s-agent -n 100

Causas más comunes: discrepancia de subnet OOBI, firewall bloqueando 6443, o JOIN_TOKEN incorrecto.

Pods de browser engine se quedan en Pending en UCS-1

kubectl describe pod -n web-agents -l app=web-agent | grep -A4 Events

Si ves node(s) didn't match Pod's node affinity/selector, el paso de re-fijado del agente no se ejecutó. Aplícalo manualmente:

kubectl patch deployment web-agent -n web-agents --type=strategic-merge-patch \
  -p '{"spec":{"template":{"spec":{"nodeSelector":{"role":"playwright"}}}}}'

Pods de synthetic-load engine se quedan en Pending en UCS-2

kubectl patch deployment k6-agent -n web-agents --type=strategic-merge-patch \
  -p '{"spec":{"template":{"spec":{"nodeSelector":{"role":"k6"}}}}}'

Personas se quedan en Pending

kubectl describe pod -n persona-shop persona-shop-* | grep -A4 Events

Si nuevamente aparece node(s) didn't match, UCS-3 no tiene la label role=ngfw-dut. Re-etiqueta:

kubectl label node ucs-3 role=ngfw-dut dut-data-plane=true --overwrite

Cloner no logra alcanzar internet

El cloner usa eth1.40 macvlan con DHCP. Verificar en UCS-3:

ip link show eth1.40
sudo dhclient -v eth1.40   # solo como diagnóstico manual — el pod tiene su propio DHCP

Si eth1.40 falta, vuelve a ejecutar setup_isp_iface (forma parte de tri-server).

node_exporter solo reporta dos de los tres nodos

kubectl get pods -n web-agents -l app.kubernetes.io/name=node-exporter -o wide

Los tres (ucs-1, ucs-2, ucs-3) deberían aparecer. Si no es así, revisa:

kubectl describe daemonset node-exporter -n web-agents | grep -A3 Tolerations

Las tolerations deben ser operator: Exists (matchea cualquier taint). Si un nodo tiene un taint personalizado que lo bloquea, agrega una toleration o quita el taint.


8. Referencia — archivo overlay

El overlay tri-node vive en overlays/tri-node/kustomization.yaml. Hereda de k8s/ y agrega seis strategic-merge patches:

patches:
  # browser-engine → UCS-1
  - target: { kind: Deployment, name: web-agent }    # role=playwright
  # synthetic-load → UCS-2
  - target: { kind: Deployment, name: k6-agent }     # role=k6
  # Servicios → UCS-3 (reutiliza role=ngfw-dut)
  - target: { kind: Deployment,  name: dashboard }   # role=ngfw-dut
  - target: { kind: StatefulSet, name: postgres }    # role=ngfw-dut
  - target: { kind: Deployment,  name: pgbouncer }   # role=ngfw-dut
  - target: { kind: Deployment,  name: cloner }      # role=ngfw-dut

Para aplicar manualmente (sin el script de instalación):

kubectl apply -k overlays/tri-node/
kubectl apply -k k8s/dut/
kubectl patch daemonset node-tuning -n web-agents \
  --type=strategic-merge-patch \
  -p '{"spec":{"template":{"spec":{"nodeSelector":{"dut-data-plane":"true"}}}}}'

Véase también