Skip to content

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

Modo de deployment: tri-node — UCS-1 executa apenas os agentes browser engine, UCS-2 executa apenas os agentes synthetic-load engine e UCS-3 hospeda personas + serviços + observabilidade. Para uma instalação em servidor único veja UBUNTU_K3S_SINGLENODE_QUICKSTART_DEPLOY.pt-BR.md; para a topologia de dois servidores veja UBUNTU_K3S_DUALNODE_QUICKSTART_DEPLOY.pt-BR.md; para a distribuída em quatro servidores veja UBUNTU_K3S_MULTINODE_QUICKSTART_DEPLOY.pt-BR.md.

Objetivo: Implantar um TLSStress.Art em exatamente três servidores Cisco UCS — um dedicado à geração de carga browser engine, um dedicado à geração de carga synthetic-load engine e um dedicado a webservers + serviços + observabilidade — fornecendo um ponto intermediário de escala entre dual-node e multi-node de quatro servidores e isolando os dois runtimes de agentes entre si.

Última verificação contra o código de produção: v3.7.0 (2026-05-12) — Ver ARCHITECTURE.md para os 37 MÓDULOs canônicos + 7 Test Kinds + arquitetura de safety DOM/CPOS/PIE-PA + a postura ZTP-prem 12/12 camadas contra operador insider (25 reivindicações de patente, partição Tier A/B, Confidential Computing, audit log selado em hash-chain, webhook de admission K8s, TPM 2.0 measured-boot, monitor DLP de egress, detector comportamental de anomalias). ADRs 0014, 0019-0025 cobrem adições pós-Freeze.

Para quem é: Engenheiros de rede e sistemas operando um test bed de NGFW que dispõem de três chassis UCS e desejam isolamento de hardware entre agentes browser engine, agentes synthetic-load engine e webservers, sem precisar de um quarto chassi para os serviços.

Estimativa de tempo: 75–105 minutos para o setup inicial do cluster; 15 minutos para teardown + redeploy.


Instalação automatizada (recomendada)

Execute o script de instalação uma vez por servidor nesta ordem exata (UCS-3 primeiro porque hospeda o control plane do k3s):

# Clone o repositório em cada servidor
git clone https://github.com/nollagluiz/AI_forSE.git && cd AI_forSE

# UCS-3 — k3s server + personas + serviços + observabilidade
sudo ./scripts/k8s-install.sh --mode=tri-server --data-iface=eth1
# ↑ Imprime JOIN_TOKEN e SERVER_IP no final — guarde-os.

# UCS-1 — apenas 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 — apenas 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 volta ao UCS-3 — aplica todos os manifestos Kubernetes
sudo ./scripts/k8s-install.sh --mode=tri-apply

Dry run: adicione --dry-run em qualquer comando para visualizar sem aplicar mudanças.

O restante deste guia explica cada passo em detalhe e é útil para customizar a instalação ou para troubleshooting da instalação automatizada.


Sumário

  1. Por que tri-node
  2. Visão geral da arquitetura
  3. Rede OOBI — obrigatória nos três UCS
  4. Pré-requisitos por servidor
  5. Passo 1 — Preparação do host (todos os três UCS)
  6. Passo 2 — Bootstrap do cluster k3s
  7. Passo 3 — Configuração de VLAN por nó
  8. Passo 4 — Aplicar manifestos
  9. Passo 5 — Verificar o deployment
  10. Observabilidade — Grafana e Prometheus em tri-node
  11. Troubleshooting
  12. Referência — arquivo de overlay

1. Por que tri-node

Escolha tri-node quando:

  • Você tem três chassis UCS disponíveis e quer isolamento de hardware entre agentes browser engine, agentes synthetic-load engine e webservers.
  • Os dois runtimes de agentes não devem competir por recursos. browser engine lança processos Chromium headless (intensivos em memória e IO); synthetic-load engine executa um único processo Go emitindo dezenas de milhares de requisições HTTP/2 + HTTP/3 (intensivo em CPU e em conexões). Compartilhar um único host significa que o runtime que primeiro saturar vai aplicar throttle no outro, e suas medições de NGFW ficam ruidosas.
  • Você não tem um quarto chassi para a camada de serviços (que seria o layout multi-node).

Comparado aos demais modos:

Single Dual Tri Multi
Quantidade de UCS 1 2 3 4
browser-engine + synthetic-load compartilham um host sim sim (ambos no UCS-1) não — UCS separados não
Serviços no host dos agentes sim não não não
Isolamento de hardware entre browser engine e synthetic-load engine
Isolamento de hardware entre agentes e webservers
Chassi dedicado para serviços

2. Visão geral da arquitetura

Topologia física

            ┌────────────────────────────────────────────────────────┐
            │      Rede OOBI — eth0 em TODOS os três UCS              │
            │              10.0.0.0/24 (exemplo)                      │
            │   k3s API :6443 · kubelet · scrape Prometheus           │
            └──────┬───────────────────┬────────────────┬─────────────┘
                   │                   │                │
          ┌────────┴───────┐  ┌────────┴────────┐  ┌────┴───────────────────┐
          │     UCS-1        │  │     UCS-2        │  │            UCS-3        │
          │  role=playwright │  │   role=k6        │  │       role=ngfw-dut     │
          │                  │  │                  │  │                          │
          │   Apenas         │  │   Apenas         │  │   20 pods de persona    │
          │   agentes        │  │   agentes        │  │   Caddy                 │
          │   browser engine     │  │   synthetic-load engine             │  │   10 slots de           │
          │                  │  │                  │  │   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      │
                  │   trunk de VLAN · ECMP  │
                  │   DSCP AF41 · MTU 9216  │
                  └──────────┬──────────────┘
                             │
                  ┌──────────┴──────────────┐
                  │  NGFW (Device Under Test)│
                  │  TLS leg-1: agentes→NGFW │
                  │  TLS leg-2: NGFW→Caddy  │
                  └─────────────────────────┘

Distribuição das cargas de trabalho

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-personas (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 + governor de CPU) ✓ DaemonSet (dut-data-plane=true) ✓ DaemonSet (dut-data-plane=true) ✓ DaemonSet (dut-data-plane=true)
cni-dhcp (DHCP ISP do cloner) ✓ DaemonSet

Por que role=ngfw-dut no UCS-3 (em vez de role=infra)

As personas sintéticas (personas/_generated/*/deployment.yaml), as cloned-personas (k8s/clone-personas/20-slots.yaml) e o SNMP exporter (k8s/dut/60-snmp-exporter.yaml) todos fixam nodeSelector: role=ngfw-dut no manifesto. Reutilizar esse label no UCS-3 evita ter que mexer em mais de vinte manifestos. O overlay tri-node então aplica patch nos serviços adicionais (Dashboard, Postgres, PgBouncer, Cloner) — que o overlay multi-node envia para role=infra — para o mesmo nó role=ngfw-dut. Esse é exatamente o mesmo esquema que o overlay dual-node usa para o UCS-2.


3. Rede OOBI — obrigatória nos três UCS

A rede OOBI (out-of-band infrastructure) é o segmento dedicado de control plane carregado por eth0 em cada UCS, independentemente do modo de deployment.

O que roda em OOBI

  • k3s API server (:6443) — UCS-1 e UCS-2 alcançam o UCS-3 por aqui para entrar no cluster
  • kubelet (:10250) — UCS-3 faz scrape de UCS-1 e UCS-2 para status dos pods
  • flannel (CNI) — tráfego pod-a-pod entre nós usa VXLAN sobre eth0
  • scrape do Prometheusnode_exporter, cAdvisor e kube-state-metrics são todos alcançados via eth0
  • renovações do cert-manager — tráfego do webhook + ACME challenge
  • Dashboard / kubectl — acesso do operador

O que NÃO roda em OOBI

  • Tráfego de teste do NGFW — esse vai pelo trunk de VLAN do eth1 (data plane)
  • Polling SNMP — VLAN 99 (192.168.90.0/24) em eth1.99
  • Egress do Cloner para a internet — VLAN 40 em eth1.40

Requisitos por UCS

UCS-1 (browser engine) UCS-2 (synthetic-load engine) UCS-3 (serviços)
IP em eth0 estático, sub-rede OOBI estático, sub-rede OOBI estático, sub-rede OOBI
Alcançável pelo eth0 dos pares
MTU 1500 (default) 1500 (default) 1500 (default)
Rota default via OOBI ✓ — egress de internet para image pulls e instalação do k3s

Se eth0 faltar em qualquer UCS, o flannel do k3s não consegue estabelecer o control plane, o nó de agente afetado não consegue entrar no cluster, e o Prometheus do UCS-3 não consegue fazer scrape das métricas do host ausente. O pre-flight check em k8s-install.sh rejeita os três modos (tri-server, tri-agent e a validação por nó dentro de tri-apply) se eth0 não existir.


4. Pré-requisitos por servidor

Hardware

UCS-1 (browser engine) UCS-2 (synthetic-load engine) UCS-3 (serviços)
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 livres em / ≥ 100 GB livres em / ≥ 500 GB livres em / (NFS + Postgres + retenção do Prometheus)

UCS-3 é o nó mais pesado. browser engine (UCS-1) é geralmente mais pesado que synthetic-load engine (UCS-2) porque cada agente lança seu próprio Chromium headless.

Sistema operacional

  • Ubuntu 22.04 LTS ou 24.04 LTS, amd64
  • Acesso de root (ou sudo)
  • Acesso à internet via rota default OOBI durante a instalação (k3s + Helm + imagens de container)

Rede — Nexus 9000

VLANs que precisam estar configuradas no trunk para os três UCS:

VLAN UCS-1 UCS-2 UCS-3 Sub-rede Uso
20 172.16.0.0/16 Agentes browser engine
30 172.17.0.0/16 Agentes synthetic-load engine
40 DHCP do ISP Egress do Cloner para a internet (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

Estritamente: a VLAN 20 deve chegar somente ao UCS-1; a VLAN 30 somente ao UCS-2; as VLANs 40, 99, 101–120 e 200–209 somente ao UCS-3. O trunk Nexus pode carregar todas elas e deixar cada lado ignorar as que não consome — o trunk é idêntico para as três portas.


Passo 1 — Preparação do host (todos os três UCS)

Preparação idêntica em UCS-1, UCS-2 e 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

# Verifique as duas NICs
ip link show eth0
ip link show eth1

Em seguida execute o script de host-tuning do projeto em cada UCS — sysctls para QUIC + TCP de alto fan-out, BBR + FQ, governor de CPU, conntrack, portas:

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

O passo apply é idempotente e persiste via drop-ins do systemd-sysctl. A cada execução de medição o alerta TestBedSysctlMissing do dashboard dispara se algum UCS regredir para os defaults do kernel.


Passo 2 — Bootstrap do cluster k3s

UCS-3 primeiro (é o k3s server)

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

O script:

  1. Faz pre-flight de eth0 + eth1 + interface ISP
  2. Cria as subinterfaces VLAN 40, 99, 101–120, 200–209 em eth1
  3. Sobe a subinterface ISP eth1.40 e persiste via netplan
  4. Instala o k3s server, faz bind da API em eth0, desabilita Traefik e ServiceLB
  5. Instala Helm, cert-manager, Multus
  6. Aplica os labels role=ngfw-dut + dut-data-plane=true no nó
  7. Imprime o JOIN_TOKEN e o SERVER_IP

Guarde as credenciais impressas — UCS-1 e UCS-2 vão precisar delas.

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

O script:

  1. Faz pre-flight de eth0 + eth1
  2. Cria a subinterface VLAN 20 em eth1 (172.16.0.1/16)
  3. Entra no k3s como agent via eth0 → UCS-3:6443
  4. Aplica os labels role=playwright + dut-data-plane=true em si mesmo

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

Igual ao UCS-1, mas cria a VLAN 30 (172.17.0.1/16) e aplica role=k6 em si mesmo.

Verifique que os três nós entraram no cluster

A partir do 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,...

Passo 3 — Configuração de VLAN por nó

O script de instalação faz isso automaticamente. 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 (serviços):

ip -br link | grep eth1\\.
# eth1.40  UP  (sem IP — DHCP via macvlan do pod cloner)
# eth1.99  UP  (192.168.90.1/24)
# eth1.101 UP  (10.1.1.1/27)
# … até eth1.120 (10.1.20.1/27)
# eth1.200 UP  (10.2.1.1/27)
# … até eth1.209 (10.2.10.1/27)

A persistência das VLANs entre reboots é tratada pelo netplan (o script escreve /etc/netplan/99-*.yaml).


Passo 4 — Aplicar manifestos

A partir do UCS-3:

# Cria o configmap da CA do NGFW (uma vez)
kubectl create configmap ngfw-ca -n web-agents --from-file=ngfw-ca.crt=<path-do-cert>

# Cria os secrets de aplicação a partir do .env
kubectl create secret generic web-agent-secrets -n web-agents --from-env-file=.env

# Aplica tudo
sudo ./scripts/k8s-install.sh --mode=tri-apply

Isso aplica:

  1. kubectl apply -k overlays/tri-node/ — manifestos base do k8s com browser engine fixado em role=playwright, synthetic-load engine fixado em role=k6 e Dashboard/Postgres/PgBouncer/Cloner fixados em role=ngfw-dut
  2. kubectl apply -k k8s/dut/ — overlay do DUT (NADs, probes SNMP, servidor NFS, DaemonSet node-tuning, ServiceMonitor do cAdvisor, regras de Prometheus para infra)
  3. Aplica patch no nodeSelector do DaemonSet node-tuning para dut-data-plane=true (executa nos três UCS)
  4. Aplica patch nos deployments browser-engine + synthetic-load via 40-playwright-patch.yaml / 50-k6-patch.yaml — anotação Multus net1 macvlan, trust da CA do NGFW, REJECT_INVALID_CERTS=true
  5. Re-fixa browser engine em role=playwright e synthetic-load engine em role=k6 (os arquivos de patch usam role=ngfw-dut por default para compatibilidade single-node — tri-node sobrescreve por runtime)
  6. Aguarda todos os pods ficarem Ready

Passo 5 — Verificar o deployment

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

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

# Pods no UCS-3 (serviços + 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 em seus próprios namespaces
kubectl get pods --all-namespaces -o wide | grep -E "persona-|clone-persona-"

# Multus net1 anexado no browser engine
kubectl exec -n web-agents deploy/web-agent -- ip -br addr | grep net1
# Esperado: um endereço 172.16.x.x

# Multus net1 anexado no synthetic-load engine
kubectl exec -n web-agents deploy/k6-agent -- ip -br addr | grep net1
# Esperado: um endereço 172.17.x.x

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

6. Observabilidade — Grafana e Prometheus em tri-node

A observabilidade roda inteiramente no UCS-3 (role=ngfw-dut):

  • Prometheus — faz scrape dos três UCS via OOBI (eth0) na porta :9100 para node_exporter, no kubelet :10250 para cAdvisor, e em :8080 para kube-state-metrics
  • Grafana — o dashboard Test-Bed Infrastructure Health se adapta automaticamente à topologia de três nós via filtros por host e regras de gravação count(node_exporter)
  • Alertas — o composto TestBedInfrastructureBottleneck, alertas a nível de host (HostUDPBufferOverflow, HostConntrackNearFull, etc.) e alertas a nível de pod (PodCPUThrottled, OOMKilled) funcionam todos out-of-the-box. Veja MONITORING_TEST_VALIDITY.pt-BR.md para o catálogo completo de alertas

O DaemonSet node_exporter não tem nodeSelector e usa tolerations: operator: Exists, então ele cobre os três UCS sem configuração adicional. O alerta NodeExporterCoverageIncomplete dispara automaticamente se algum nó parar de reportar métricas de host.

Em tri-node especificamente, os painéis do Grafana naturalmente separam as métricas de host do browser engine (UCS-1) das do synthetic-load engine (UCS-2) por causa da dimensão do label node — você consegue ver imediatamente se um runtime é o gargalo enquanto o outro está ocioso, algo que uma topologia dual-node com agentes em um único host não permite enxergar.

Para abrir o Grafana a partir da estação do operador (assumindo acesso kubectl ao UCS-3):

kubectl port-forward -n web-agents svc/grafana 3000:3000
# em seguida visite http://localhost:3000

7. Troubleshooting

UCS-1 ou UCS-2 não consegue entrar no cluster

Sintomas: a instalação tri-agent trava em Joining k3s cluster as agent.

# No UCS com falha — verifique alcançabilidade OOBI até o UCS-3
ping <UCS-3-OOBI-IP>
nc -vz <UCS-3-OOBI-IP> 6443

# No UCS com falha — verifique status e logs do serviço k3s-agent
sudo systemctl status k3s-agent
sudo journalctl -u k3s-agent -n 100

Causas mais comuns: divergência de sub-rede OOBI, firewall bloqueando 6443 ou JOIN_TOKEN errado.

Pods browser engine ficam em Pending no UCS-1

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

Se aparecer node(s) didn't match Pod's node affinity/selector, o passo de re-pinning dos agentes não rodou. Aplique manualmente:

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

Pods synthetic-load engine ficam em Pending no UCS-2

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

Personas ficam em Pending

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

Se aparecer node(s) didn't match novamente, o UCS-3 não está com o label role=ngfw-dut. Re-aplique o label:

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

Cloner não consegue alcançar a internet

O Cloner usa macvlan em eth1.40 com DHCP. Verifique no UCS-3:

ip link show eth1.40
sudo dhclient -v eth1.40   # apenas como diagnóstico manual — o pod tem seu próprio DHCP

Se eth1.40 estiver ausente, rode novamente setup_isp_iface (ele faz parte do tri-server).

node_exporter reporta apenas dois dos três nós

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

Os três (ucs-1, ucs-2, ucs-3) devem aparecer. Caso não, verifique:

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

As tolerations devem ser operator: Exists (cobre qualquer taint). Se algum nó tiver um taint customizado bloqueando o pod, adicione uma toleration ou remova o taint.


8. Referência — arquivo de overlay

O overlay tri-node fica em overlays/tri-node/kustomization.yaml. Ele herda de k8s/ e adiciona seis strategic-merge patches:

patches:
  # browser-engine → UCS-1
  - target: { kind: Deployment, name: web-agent }    # role=playwright
  # K6 → UCS-2
  - target: { kind: Deployment, name: k6-agent }     # role=k6
  # Serviços → 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 (sem o script de instalação):

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"}}}}}'

Veja também