ADR 0001 — Coleta de métricas do cluster k3s via OpenTelemetry
| Campo | Valor |
|---|---|
| Status | Aceito |
| Data | 2026-03-21 |
| Afeta | homelab, contabo-monitoring-server |
| Depende de | infra 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:
- Expor uma nova porta na VPS principal (aumenta superfície de ataque)
- Configurar mTLS individualmente para aquela porta no
prometheus.yml - 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
| Fonte | Receiver OTel | O que coleta |
|---|---|---|
| Node | hostmetrics | CPU, memória, disco, filesystem, rede, load average |
| Kubernetes | kubeletstats | métricas de pods e containers via kubelet (sem porta externa) |
| Kubernetes | k8s_cluster | estado de deployments, replicasets, nodes via kube-apiserver (sem porta externa) |
| Traefik | otlp (push nativo) | requisições, latência, erros por rota — Traefik empurra diretamente para o OTel Collector |
| Nginx | prometheus (scrape interno) | conexões, requisições, status — via nginx-prometheus-exporter ou stub_status |
| PostgreSQL | postgresql | conexõ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ção | Comportamento |
|---|---|
| Monitoring cluster inacessível | OTel enfileira até queue_size: 50 batches, depois descarta |
OTel atinge limit_mib: 100 | memory_limiter recusa novos dados — CPU/memória da VPS protegidos |
| Pod OTel reinicia | Fila em memória é perdida — janela de dados perdidos = tempo de restart |
| Nó k3s sem recursos | Resource 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-contribcom resource limits fixos - Ajuste no monitoring cluster: Habilitar OTLP receiver no Prometheus e abrir porta
4318internamente (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)