Skip to content

TLSStress.Art — Deployment Dual-Node em 2 Servidores UCS

Modo de deployment: dual-node — UCS-1 roda a frota de agentes (browser-engine + synthetic-load) e UCS-2 roda todo o restante (personas, serviços, observabilidade). Para um setup de servidor único veja UBUNTU_K3S_SINGLENODE_QUICKSTART_DEPLOY.pt-BR.md; para a topologia distribuída de quatro servidores veja UBUNTU_K3S_MULTINODE_QUICKSTART_DEPLOY.pt-BR.md.

Objetivo: Subir um TLSStress.Art distribuído em exatamente dois servidores Cisco UCS — um dedicado à geração de carga, outro dedicado a webservers + serviços + observabilidade — para oferecer um ponto de escala intermediário entre single-node e multi-node de quatro servidores.

Ú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 dois chassis UCS e querem isolamento de hardware entre os geradores de carga e os webservers sob medição.

Tempo estimado: 60–90 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-2 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-2 — k3s server + personas + serviços + observabilidade
sudo ./scripts/k8s-install.sh --mode=dual-server --data-iface=eth1
# ↑ Imprime JOIN_TOKEN e SERVER_IP no final — guarde para usar no UCS-1.

# UCS-1 — agentes browser-engine + synthetic-load
sudo ./scripts/k8s-install.sh --mode=dual-agent \
     --server-ip=<UCS-2-OOBI-IP> --token=<JOIN_TOKEN> --data-iface=eth1

# De volta no UCS-2 — aplica todos os manifests Kubernetes
sudo ./scripts/k8s-install.sh --mode=dual-apply

Dry run: acrescente --dry-run em qualquer comando para visualizar sem fazer alterações.

O restante deste guia detalha cada passo e é útil para customizar o setup ou diagnosticar problemas da instalação automatizada.


Sumário

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

1. Por que dual-node

Single-node é o layout mais simples, mas todos os workloads competem pela mesma CPU, memória e filas de NIC — sob carga não dá para distinguir se o gargalo está no NGFW ou no próprio test bed. Multi-node resolve isso completamente, mas exige quatro chassis UCS.

Dual-node é o meio-termo:

  • Isolamento de hardware: geradores de carga (browser-engine + synthetic-load) e webservers (personas + cloned-personas) rodam em chassis UCS físicos diferentes, então o único caminho de contenção entre eles é o NGFW sob teste — exatamente a variável que se quer medir.
  • Dois papéis em vez de quatro: metade do espaço em rack, metade do cabeamento, metade da carga operacional do multi-node.
  • Mesma observabilidade: dashboards Grafana e alertas Prometheus idênticos aos do single-node e multi-node — o node_exporter roda em ambos os UCS via DaemonSet, e o alerta de gargalo do test-bed se auto-adapta à topologia de dois nós.

Escolha dual-node quando você tem dois chassis e quer medições limpas do NGFW sem montar um cluster de quatro servidores.


2. Visão geral da arquitetura

Topologia física

                ┌──────────────────────────────────────────────────┐
                │      Rede OOBI — eth0 nos DOIS UCS                │
                │             10.0.0.0/24 (exemplo)                 │
                │   k3s API :6443 · kubelet · scrape Prometheus     │
                └────────┬──────────────────────┬───────────────────┘
                         │                      │
                ┌────────┴────────┐    ┌────────┴────────────────────┐
                │     UCS-1        │    │           UCS-2              │
                │   role=agents    │    │       role=ngfw-dut          │
                │                  │    │                              │
                │   browser engine     │    │   20 pods Caddy de personas  │
                │   synthetic-load engine             │    │   10 slots cloned-persona    │
                │                  │    │   Dashboard · Postgres       │
                │                  │    │   PgBouncer · Cloner · NFS   │
                │                  │    │   Grafana · Prometheus       │
                │                  │    │   SNMP exporter              │
                │  eth0 (OOBI)     │    │   eth0 (OOBI)                │
                │  eth1 (trunk)    │    │   eth1 (trunk)               │
                └────────┬─────────┘    └────────────┬─────────────────┘
                         │                           │
                  VLAN 20 (172.16/16)        VLAN 99 mgmt (192.168.90/24)
                  VLAN 30 (172.17/16)        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 de workloads

Workload UCS-1 (role=agents) UCS-2 (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
node-tuning (sysctl + BBR + CPU governor) ✓ DaemonSet (dut-data-plane=true) ✓ DaemonSet (dut-data-plane=true)
cni-dhcp (DHCP ISP do cloner) ✓ DaemonSet

Por que role=ngfw-dut no UCS-2 (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) têm nodeSelector: role=ngfw-dut fixado. Reaproveitar essa label no UCS-2 evita mexer em vinte e tantos manifests. O overlay dual-node então faz o patch dos workloads de serviços adicionais (Dashboard, Postgres, PgBouncer, Cloner) — que o overlay multi-node envia para role=infra — para o mesmo nó role=ngfw-dut.


3. Rede OOBI — obrigatória nos dois 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

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

O que NÃO roda em OOBI

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

Requisitos por UCS

UCS-1 (agentes) UCS-2 (serviços)
IP em eth0 estático, subnet OOBI estático, subnet OOBI
Alcançável a partir do eth0 do par ✓ (kubelet ↔ control plane)
MTU 1500 (default) 1500 (default)
Rota default via OOBI ✓ — egress de internet para pull de imagens e instalação do k3s

Se eth0 faltar em qualquer UCS, o flannel do k3s não consegue estabelecer o control plane, o UCS-1 não consegue entrar no cluster e o Prometheus no UCS-2 não consegue fazer scrape de métricas de host do UCS-1. O pre-flight check em k8s-install.sh rejeita tanto dual-server quanto dual-agent se eth0 estiver ausente.


4. Pré-requisitos por servidor

Hardware

UCS-1 UCS-2
CPU ≥ 16 cores físicos ≥ 32 cores físicos
RAM ≥ 64 GB ≥ 128 GB
NICs eth0 (OOBI) + eth1 (trunk para Nexus) eth0 (OOBI) + eth1 (trunk para Nexus)
Disco ≥ 200 GB livres em / ≥ 500 GB livres em / (NFS + Postgres + retenção do Prometheus)

UCS-2 é o nó mais pesado — roda personas, todos os serviços e o stack de observabilidade. Planeje 2× os recursos do UCS-1.

Sistema operacional

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

Rede — Nexus 9000

VLANs que precisam estar configuradas no trunk para ambos os UCS:

VLAN UCS-1 UCS-2 Subnet 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 de internet do 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 cloned-persona

A rigor: VLAN 20 e VLAN 30 só podem chegar no UCS-1; VLANs 40, 99, 101–120 e 200–209 só podem chegar no UCS-2. O trunk Nexus pode carregar todas e deixar cada lado ignorar as que não consome — o trunk é idêntico para as duas portas.


Passo 1 — Preparação do host (ambos UCS)

Preparação idêntica em UCS-1 e UCS-2:

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

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

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

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

A etapa apply é idempotente e persiste via drop-ins do systemd-sysctl. A cada rodada de medição o alerta TestBedSysctlMissing do dashboard dispara se algum dos UCS regredir aos defaults do kernel.


Passo 2 — Bootstrap do cluster k3s

UCS-2 primeiro (é o k3s server)

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

O script:

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

Salve as credenciais impressas — UCS-1 vai precisar delas.

UCS-1 (agentes)

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

O script:

  1. Faz pre-flight de eth0 + eth1 (ambos obrigatórios)
  2. Cria subinterfaces de VLAN 20, 30 em eth1
  3. Entra no k3s como agent via eth0 → UCS-2:6443
  4. Aplica labels role=agents + dut-data-plane=true em si mesmo

Verifique a partir do UCS-2:

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

Passo 3 — Setup de VLAN por nó

O script de instalação faz isso automaticamente. Para verificar:

UCS-1:

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

UCS-2:

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)

Persistência das VLANs entre reboots é feita pelo netplan (o script grava /etc/netplan/99-*.yaml).


Passo 4 — Aplicar manifests

A partir do UCS-2:

# 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=dual-apply

Isso aplica:

  1. kubectl apply -k overlays/dual-node/ — manifests k8s base com browser-engine/synthetic-load fixados em role=agents e Dashboard/Postgres/PgBouncer/Cloner fixados em role=ngfw-dut
  2. kubectl apply -k k8s/dut/ — overlay DUT (NADs, probes SNMP, servidor NFS, DaemonSet node-tuning, ServiceMonitor cAdvisor, regras Prometheus de infra)
  3. Faz patch do nodeSelector do DaemonSet node-tuning para dut-data-plane=true (roda nos dois UCS)
  4. Faz patch dos 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 + synthetic-load em role=agents (os arquivos de patch usam role=ngfw-dut por padrão para compatibilidade com single-node — dual-node sobrescreve)
  6. Espera todos os pods ficarem Ready

Passo 5 — Verificar o deployment

# Pods no UCS-1 (agentes)
kubectl get pods -n web-agents -o wide --field-selector spec.nodeName=ucs-1
# Esperado: somente web-agent-* e k6-agent-*

# Pods no UCS-2 (serviços + personas)
kubectl get pods -n web-agents -o wide --field-selector spec.nodeName=ucs-2
# 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 nos agentes
kubectl exec -n web-agents deploy/web-agent -- ip -br addr | grep net1
# Esperado: um endereço 172.16.x.x

# Confere que o servidor NFS está alcançável a partir do cloner
kubectl exec -n web-agents deploy/cloner -- mount | grep nfs

# Smoke test fim a fim — agentes alcançam uma persona via NGFW
kubectl exec -n web-agents deploy/web-agent -- \
  curl -sf https://shop.persona.local/ | head -1

6. Observabilidade — Grafana e Prometheus em dual-node

A observabilidade roda integralmente no UCS-2 (role=ngfw-dut):

  • Prometheus — faz scrape dos dois UCS via OOBI (eth0) na porta :9100 para node_exporter, kubelet :10250 para cAdvisor e :8080 para kube-state-metrics
  • Grafana — dashboard Test-Bed Infrastructure Health se auto-adapta à topologia de dois nós via filtros por host e regras de recording count(node_exporter)
  • Alertas — o composto TestBedInfrastructureBottleneck, alertas a nível de host (HostUDPBufferOverflow, HostConntrackNearFull, etc.) e 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 cobre os dois UCS sem configuração adicional. O alerta NodeExporterCoverageIncomplete dispara automaticamente se algum dos nós parar de reportar métricas de host.

Para abrir o Grafana a partir da workstation do operador (assumindo que você tenha acesso kubectl ao UCS-2):

kubectl port-forward -n web-agents svc/grafana 3000:3000
# então acesse http://localhost:3000

7. Troubleshooting

UCS-1 não consegue entrar no cluster

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

# No UCS-1 — checa alcance OOBI até o UCS-2
ping <UCS-2-OOBI-IP>
nc -vz <UCS-2-OOBI-IP> 6443

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

Causas mais comuns: subnet OOBI inconsistente, firewall bloqueando 6443, ou JOIN_TOKEN errado.

Pods browser engine ficam em Pending

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, a etapa de re-fixar os agentes não rodou. Aplique manualmente:

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

Personas ficam em Pending

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

Se aparecer node(s) didn't match de novo, o UCS-2 não está com a label role=ngfw-dut. Aplique a label novamente:

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

Cloner não alcança a internet

O cloner usa macvlan em eth1.40 com DHCP. Verifique no UCS-2:

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

Se eth1.40 estiver faltando, rode setup_isp_iface novamente (faz parte de dual-server).

node_exporter reporta apenas um nó

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

Tanto ucs-1 quanto ucs-2 devem aparecer. Se não, verifique:

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

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


8. Referência — arquivo de overlay

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

patches:
  # browser-engine + K6 → UCS-1
  - target: { kind: Deployment, name: web-agent }   # role=agents
  - target: { kind: Deployment, name: k6-agent }    # role=agents
  # Serviços → UCS-2 (reaproveita 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/dual-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