Skip to main content

ADR 0001 — Coleta de métricas do cluster k3s via OpenTelemetry

CampoValor
StatusAceito
Data2026-03-21
Afetahomelab, contabo-monitoring-server
Depende deinfra de mTLS existente em contabo-monitoring-server/certs/

Contexto

O cluster principal roda k3s em uma VPS com recursos limitados (RAM e CPU restritos). As métricas do cluster precisam chegar ao cluster de monitoring (Contabo VPS), que já executa Prometheus + Grafana.

Hoje o Prometheus no cluster de monitoring faz scrape direto (pull) de cada exporter exposto na VPS principal via mTLS:

Prometheus (monitoring) ──mTLS──► :9100  node-exporter
──mTLS──► :8080 kube-state-metrics
──mTLS──► :???? (próximo exporter)

O problema central desse modelo é que cada nova fonte de métricas exige:

  1. Expor uma nova porta na VPS principal (aumenta superfície de ataque)
  2. Configurar mTLS individualmente para aquela porta no prometheus.yml
  3. Gerenciar um certificado separado por endpoint

Conforme o cluster cresce, isso se torna difícil de manter e aumenta o número de portas abertas desnecessariamente.

Requisitos adicionais:

  • Não sobrecarregar a VPS — a coleta não pode consumir recursos além de um limite fixo
  • Resiliência a falhas de rede — se o envio cair, o coletor deve degradar graciosamente sem crescer ilimitadamente em memória

Decisão

Instalar um OpenTelemetry Collector no cluster k3s configurado para coletar métricas de todas as fontes internamente e enviá-las ao cluster de monitoring via uma única conexão de saída mTLS:

VPS k3s
└── OTel Collector
├── hostmetrics ← CPU, memória, disco, rede do nó
├── kubeletstats ← pods, containers (via kubelet interno)
├── k8s_cluster ← deployments, replicasets, nodes (via kube-apiserver interno)
├── otlp receiver ◄─ Traefik push nativo (cluster-internal)
├── prometheus scrape ← Nginx stub_status / nginx-prometheus-exporter (cluster-internal)
├── postgresql receiver ← métricas do Postgres (conexão interna ao service)
└── exporta via OTLP/HTTP ──mTLS──► Prometheus (monitoring) :4318

Isso inverte o modelo: ao invés do monitoring abrir conexões para cada porta da VPS, a VPS abre uma única conexão de saída com um único certificado. Novas fontes de métricas são adicionadas como receivers no OTel — sem abrir novas portas.

No cluster de monitoring, habilitar o OTLP receiver no Prometheus (--enable-feature=otlp-write-receiver) para receber as métricas sem adicionar outro processo.


Detalhes de implementação

Fontes de métricas

FonteReceiver OTelO que coleta
NodehostmetricsCPU, memória, disco, filesystem, rede, load average
Kuberneteskubeletstatsmétricas de pods e containers via kubelet (sem porta externa)
Kubernetesk8s_clusterestado de deployments, replicasets, nodes via kube-apiserver (sem porta externa)
Traefikotlp (push nativo)requisições, latência, erros por rota — Traefik empurra diretamente para o OTel Collector
Nginxprometheus (scrape interno)conexões, requisições, status — via nginx-prometheus-exporter ou stub_status
PostgreSQLpostgresqlconexões, transações, tamanho de tabelas — via connection string interna

Nenhuma dessas coletas exige abrir portas no nó — tudo trafega dentro da rede do cluster ou via loopback.

Pipeline OTel no cluster k3s

receivers:
hostmetrics:
collection_interval: 60s
scrapers:
cpu: {}
memory: {}
disk: {}
filesystem: {}
network: {}
load: {}

kubeletstats:
collection_interval: 60s
auth_type: serviceAccount
endpoint: "${env:K8S_NODE_NAME}:10250"
insecure_skip_verify: true

k8s_cluster:
collection_interval: 60s
auth_type: serviceAccount

otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317" # Traefik envia métricas aqui (cluster-internal)

prometheus:
config:
scrape_configs:
- job_name: nginx
scrape_interval: 60s
static_configs:
- targets: ['nginx-exporter.<namespace>.svc.cluster.local:9113']

postgresql:
endpoint: "postgres.<namespace>.svc.cluster.local:5432"
username: "${env:PG_USER}"
password: "${env:PG_PASSWORD}"
databases: [kelbi]
collection_interval: 60s
tls:
insecure: true # tráfego interno ao cluster

processors:
memory_limiter: # deve ser o PRIMEIRO processor
limit_mib: 100 # hard limit — OOM-safe
spike_limit_mib: 20
check_interval: 1s
batch:
timeout: 30s # agrupa envios para reduzir overhead de rede
send_batch_size: 512

exporters:
otlphttp:
endpoint: "https://<monitoring-ip>:4318"
tls:
ca_file: /certs/ca.crt
cert_file: /certs/otel-collector.crt
key_file: /certs/otel-collector.key
retry_on_failure:
enabled: true
initial_interval: 10s
max_interval: 60s
max_elapsed_time: 0 # tenta indefinidamente
sending_queue:
enabled: true
num_consumers: 2
queue_size: 50 # fila limitada — descarta ao invés de crescer sem controle

service:
pipelines:
metrics:
receivers: [hostmetrics, kubeletstats, k8s_cluster, otlp, prometheus, postgresql]
processors: [memory_limiter, batch]
exporters: [otlphttp]

Resource limits no pod (k8s)

resources:
requests:
cpu: 25m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi

mTLS

Usar a infra existente: make gen-server-cert NAME=otel-collector IP=<ip> gera o par de chaves assinado pela CA do contabo-monitoring-server. O Prometheus no monitoring cluster já conhece essa CA.

No Prometheus (monitoring cluster), adicionar o OTLP receiver e protegê-lo com mTLS:

# prometheus.yml
otlp:
protocols:
http:
tls_config:
ca_file: /certs/ca.crt
cert_file: /certs/prometheus.crt
key_file: /certs/prometheus.key
client_auth_type: RequireAndVerifyClientCert

Comportamento sob falha

SituaçãoComportamento
Monitoring cluster inacessívelOTel enfileira até queue_size: 50 batches, depois descarta
OTel atinge limit_mib: 100memory_limiter recusa novos dados — CPU/memória da VPS protegidos
Pod OTel reiniciaFila em memória é perdida — janela de dados perdidos = tempo de restart
Nó k3s sem recursosResource limits impedem que OTel consuma além do alocado

A perda de métricas durante falhas é aceitável. O objetivo é nunca deixar a coleta de métricas degradar o cluster de produção.


Alternativas consideradas

Manter pull via Prometheus (status quo)

Cada nova fonte de métricas exige expor uma nova porta na VPS e configurar mTLS individualmente no prometheus.yml. Não escala conforme o cluster cresce. Descartado.

Prometheus Agent Mode

Prometheus em modo agent faz remote_write para o monitoring — resolve o problema de portas, pois também usa uma única conexão de saída. Mas o Agent só coleta via scrape, o que significa que ainda precisaria de exporters rodando com portas abertas internamente. Não centraliza o certificado com a mesma efetividade. Descartado.

OTel Collector no monitoring como receiver + scraper do cluster k3s

Manteria o modelo pull — o OTel no monitoring abriria conexões para a VPS. Não resolve o problema de portas expostas nem centraliza o certificado. Descartado.


Consequências

  • Novo componente no cluster k3s: Deployment do otelcol-contrib com resource limits fixos
  • Ajuste no monitoring cluster: Habilitar OTLP receiver no Prometheus e abrir porta 4318 internamente (não exposta publicamente)
  • Novos certificados: make gen-server-cert NAME=otel-collector IP=<ip> + distribuição via Secret k8s
  • Scrapes existentes no prometheus.yml podem ser removidos após validar o fluxo OTel (node-exporter-arcamh-prod, kube-state-metrics-arcamh-prod)