Pular para o conteúdo

Instalar o Profiler

O RootPilot Profiler é o segundo artefato: opcional, privilegiado, e instalado por quem quer o eixo comportamental — o que mudou dentro do processo entre dois deploys, onde métrica e log mostram só o sintoma.

Ele é uma imagem diferente do Agent e exige privilégio que o Agent não pede. Antes de seguir, leia Segurança → RootPilot Profiler: é lá que estão a tabela de permissões, o que sai da sua infraestrutura e por que não existe um modo reduzido. Esta página é o “como”, e supõe que a decisão já foi tomada.

  1. Ligue o Profiler na organização. Em Configurações → RootPilot Profiler, no app. Sem isso o enrollment é recusado com 403 antes de qualquer coisa — o privilégio de kernel é concessão deliberada e registrada, não efeito colateral de um deploy.
  2. Escreva a lista de alvos. Ela é obrigatória e o Profiler não sobe sem ela. Ver abaixo.
  3. Tenha um bootstrap token do tenant, como no Agent.
Janela do terminal
# AWS (ECR)
docker pull <conta-rootpilot>.dkr.ecr.<região>.amazonaws.com/rootpilot-profiler:<versão>
# Google Cloud (Artifact Registry)
docker pull <região>-docker.pkg.dev/<projeto-rootpilot>/rootpilot/rootpilot-profiler:<versão>

Multi-arch (amd64 + arm64) e assinada com cosign, como a do Agent — o procedimento de verificação é o mesmo descrito em A imagem.

A imagem embarca o coletor (ptop) e o agregador (witness). O processo que fala com o control-plane roda como usuário não-root e sem capability nenhuma; quem carrega o privilégio é o binário do coletor, por file capability.

Um JSON, montado no container, apontado por RUNNER_PROFILE_TARGETS_FILE:

{
"targets": [
{ "workload": "checkout-api" },
{ "namespace": "producao", "workload": "pricing-engine" }
]
}

Ela é sua e nunca passa por nós. Não existe default que signifique “tudo o que estiver no nó”: o Profiler recusa subir com arquivo ausente, com lista vazia, ou com "workload": "*". O schema é estrito — qualquer chave desconhecida, inclusive um campo de comentário bem-intencionado, derruba o boot.

Arquivo e não variável de ambiente de propósito: no seu repositório de infraestrutura ele é diffável, entra numa revisão e alguém olha. É literalmente a lista de onde o container privilegiado pode encostar.

services:
profiler:
image: <conta-rootpilot>.dkr.ecr.<região>.amazonaws.com/rootpilot-profiler:<versão>
restart: unless-stopped
environment:
RUNNER_TENANT: sua-org
RUNNER_INSTANCE_ID: sua-org-profiler01
RUNNER_TUNNEL_URL: wss://tunnel.rootpilot.sh:8443
RUNNER_ENROLL_URL: https://app.rootpilot.sh
RUNNER_IDENTITY_DIR: /identity
RUNNER_PROFILE_TARGETS_FILE: /etc/rootpilot/profile-targets.json
# As cinco, e nem uma a mais. A ausência de qualquer uma derruba o boot com o nome dela e o que
# ela custa — ver a página de segurança.
cap_add: [BPF, PERFMON, SYS_PTRACE, DAC_READ_SEARCH, SYS_ADMIN]
# Visibilidade, não privilégio: sem isto o container só enxerga os próprios processos, e o alvo
# simplesmente não existe para o coletor.
pid: host
security_opt:
- apparmor=rootpilot-profiler # ver "AppArmor", abaixo
volumes:
- ./identity:/identity
- ./profile-targets.json:/etc/rootpilot/profile-targets.json:ro
# A biblioteca de eBPF procura os tracepoints aqui. Sem estes dois o coletor carrega e **não
# anexa** — e a captura sai com eixos faltando, que é pior que sair vazia.
- /sys/kernel/tracing:/sys/kernel/tracing
- /sys/kernel/debug:/sys/kernel/debug
mem_limit: 512m

O bootstrap token entra como no Agent (RUNNER_BOOTSTRAP_TOKEN, de preferência via secret store, não em texto no compose).

Se o seu host tem AppArmor ativo — Ubuntu e Debian têm, por default — o perfil docker-default bloqueia o que o Profiler precisa fazer: ele só permite ptrace entre containers do mesmo perfil, e o seu alvo normalmente não é um (um processo do host é unconfined; um container de outro runtime tem outro perfil).

Não use apparmor=unconfined: isso desfaz o confinamento inteiro por causa de uma regra. Carregue um perfil que é o docker-default mais uma linha:

#include <tunables/global>
profile rootpilot-profiler flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
# … o corpo do docker-default …
# A única diferença: ler /proc/<pid>/{exe,ns/*} de um alvo fora deste perfil.
ptrace (read) peer=unconfined,
}
Janela do terminal
sudo apparmor_parser -r -W /etc/apparmor.d/rootpilot-profiler

Como saber se é isso: uma captura volta com os coletores em permission denied sobre /proc/<pid>/ns/* ou /proc/<pid>/exe, e o kernel registra a negação:

apparmor="DENIED" operation="ptrace" profile="docker-default" comm="ptop" peer="unconfined"

O pod precisa de:

  • hostPID: true (ou shareProcessNamespace se o alvo estiver no mesmo pod). Sem isso o coletor só enxerga a si mesmo.
  • As cinco capabilities em securityContext.capabilities.add.
  • /sys/kernel/tracing e /sys/kernel/debug montados do host, como hostPath.
  • Um perfil AppArmor que permita ptrace (read) contra o perfil do alvo. Em clusters com containerd, o default é cri-containerd.apparmor.d, e o mesmo raciocínio da seção acima se aplica — o perfil precisa ser carregado em cada nó e referenciado no securityContext.appArmorProfile.
  • Um volume para /identity, com a mesma vida do pod que o Agent precisa — e não compartilhado com outro artefato, pelo motivo da caixa acima. Num Deployment isso quer dizer um PVC por réplica, ou um StatefulSet.

Nenhuma permissão da API do Kubernetes é necessária hoje. O gatilho automático (perceber o pod subir com a imagem nova) não existe — quando existir, ele pedirá get/list/watch em pods, só nos namespaces da sua allowlist, e a página de segurança será atualizada antes.

DaemonSet: o roteamento existe, o cluster não foi exercitado

Seção intitulada “DaemonSet: o roteamento existe, o cluster não foi exercitado”

Subir não é medir, e a diferença é a que mais custa: um coletor que não anexa produz captura com eixos faltando, e um eixo faltando comparado contra uma captura completa se lê como comportamento que parou.

Duas checagens, nessa ordem:

  1. O boot. Ele declara a classe e quantos alvos leu, e recusa subir nomeando a capability que faltar — não existe caminho em que ele suba verde e capture cego por falta de permissão.
  2. A primeira captura. Abra-a em Profiling, no app. A tela declara a instrumentação: se ela disser N de M probes em vez de instrumentação completa, o bloco abaixo nomeia cada coletor que não anexou e o motivo — normalmente AppArmor ou os mounts de tracefs.

Uma captura com instrumentação incompleta não é uma captura mais fraca: é uma captura com eixos que não existem, e o produto prefere dizer isso a comparar mundos diferentes.

O gatilho é explícito: o Profiler não decide sozinho quando medir. Quem pede é o seu pipeline — logo depois de publicar uma versão — ou alguém do seu time durante uma investigação. São duas formas, e elas têm portas diferentes porque a pressa é diferente.

Em Profiling, no topo da página, há um formulário: processo, versão e ambiente. Qualquer membro da organização pode pedir — é o mesmo acesso que abre a página, e a decisão de owner/admin já foi tomada antes, ao ligar o Profiler. O desfecho aparece no registro logo abaixo, com quem pediu ao lado.

Janela, warmup e captura de réplica não estão no formulário de propósito: quem precisa deles está escrevendo pipeline, e o pipeline tem a chamada abaixo.

Em Configurações → Tokens de Captura do Profiler, no app. Ele começa com rpcap_, é mostrado uma única vez na criação, e é distinto do token de MCP e do de ingress de incidentes: não lê nada, só enfileira um pedido de captura.

Dê um token a cada pipeline — e um separado para uso humano, se o seu time for pedir captura durante investigação. O nome é o que aparece no registro da captura, e é ele que responde “quem pediu isto”.

Janela do terminal
curl -X POST https://app.rootpilot.sh/api/profiling/captures \
-H "Authorization: Bearer $ROOTPILOT_CAPTURE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"workload": "checkout-api",
"version": "v2.14.0",
"env": "producao",
"duration_sec": 60
}'
campo obrigatório o que é
workload sim o nome do processo, exatamente como está na sua lista de alvos. Fora dela, o Profiler recusa.
version sim a versão que acabou de subir. É a chave do diff — a captura de v2.14.0 é comparada com a de v2.13.0.
env não producao, staging… Vazio é legítimo: nem toda frota tem o conceito.
kind não (head) head é a captura da versão. replicate é a mesma versão medida de novo, e é o que sustenta o piso de ruído e a janela de confirmação — chame algumas vezes se quiser um diff mais confiante.
duration_sec não (60) entre 5 e 300.
warmup_sec não (5) entre 0 e 60 — o tempo descartado antes de começar a medir.

duration_sec/durationSec e warmup_sec/warmupSec são aceitos nas duas grafias, e o token também pode ir como ?token= — alguns passos declarativos de CI não deixam pôr header.

As respostas, e cada uma pede uma ação diferente:

  • 202 — {"request_id": "…", "status": "queued", "deduped": false}. O pedido entrou na fila e é drenado em segundos. deduped: true quer dizer que já havia um pedido igual em voo e você recebeu o id dele: repetir a chamada não empilha capturas.
  • 400 — o corpo não passou na validação, e a mensagem nomeia o campo.
  • 401 — token ausente, inválido ou revogado.
  • 403 — o Profiler não está habilitado na organização. É o opt-in, em Configurações.
  • 429 — o teto é de 20 pedidos por hora por organização, e a resposta traz retry_after_seconds. O teto é por organização e não por IP de propósito: quem paga a conta é o processo observado, não a rede de onde o pedido saiu.

O 202 diz que o pedido entrou, não que a captura aconteceu. Pode não haver Profiler vivo naquele instante (o pedido espera), e o alvo pode não estar rodando em nó nenhum. O desfecho — pushed, nothing_to_push ou failed, com o motivo ao lado — aparece em Profiling, no app.

Dizemos isto aqui porque a alternativa é você descobrir sozinho:

  • Kubernetes, em nenhum cluster.
  • JVM: o eixo de símbolo para a JVM está implementado e testado unitariamente, e nunca rodou contra uma JVM viva.

Docker em amd64 e arm64 é o que está exercitado.