Pular para o conteúdo

Kubernetes

A imagem é construída por apps/runner/Dockerfile, com contexto na raiz do repositório. O entrypoint é apps/runner/docker-entrypoint.sh e o comando é:

node --import @rootpilotsh/otel/register apps/runner/dist/index.js

A imagem roda como node (uid 1000), não root — e é por isso que o Compose monta ~/.aws em /home/node/.aws. O destino do mount é acoplado ao USER da imagem: a cadeia default do SDK resolve o cache do SSO a partir de $HOME, e não há env var que reaponte esse cache. Se você montar o ~/.aws em outro lugar, monte no $HOME do usuário que o pod roda.

O runner é cliente de saída. Ele não escuta em porta nenhuma, não expõe /healthz e não tem endpoint de readiness.

Consequência direta: não configure httpGet em liveness ou readiness probes: todos vão falhar. Se você precisa de um sinal, ele vem dos logs de boot (boot: ready) e do Fleet do control-plane, que enxerga o runner conectado. A saúde por connector também é reportada no handshake, não por HTTP.

Requisito 2: graça de terminação maior que o drain

Seção intitulada “Requisito 2: graça de terminação maior que o drain”

O runner trata SIGTERM drenando: ele para de aceitar trabalho novo, termina o que está em voo e só então sai. O teto disso é RUNNER_DRAIN_TIMEOUT_MS, cujo default é 120 000 ms.

O default do Kubernetes para terminationGracePeriodSeconds é 30 s. Se você deixar assim, o SIGKILL chega no meio do drain.

terminationGracePeriodSeconds: 150 # > RUNNER_DRAIN_TIMEOUT_MS (120s), com folga

Um reclaim duro sem drain (SIGKILL, OOM, spot) perde o estado aprendido desde o último flush. O RUNNER_LEARNED_FLUSH_INTERVAL_MS (default 60 s) existe justamente para limitar essa perda, mas não a elimina.

O runner é cattle e não tem estado durável. O cache SQLite do learned-store é :memory: por default. Não crie PVC. Se você apontar RUNNER_LEARNED_STORE_PATH para um caminho em disco, saiba que está criando um cache, não uma fonte de verdade: a fonte de verdade é o control-plane.

Este é o ponto em que Kubernetes muda o desenho, e para melhor.

O runner lê o token OIDC projetado da ServiceAccount do Kubernetes e o envia como atestação. Nada secreto entra na imagem: o token é um JWT de vida curta, com audiência fixa, que o kubelet projeta.

O caminho default é /var/run/secrets/tokens/gke-oidc/token, configurável por RUNNER_GKE_TOKEN_PATH. Ou seja, o pod precisa de um volume projected com serviceAccountToken montado exatamente ali — e projetado na audiência rootpilot-enroll:

volumes:
- name: rootpilot-token
projected:
sources:
- serviceAccountToken:
path: token
audience: rootpilot-enroll # o control-plane exige esta audiência
expirationSeconds: 3600
containers:
- name: rootpilot-agent
volumeMounts:
- name: rootpilot-token
mountPath: /var/run/secrets/tokens/gke-oidc
readOnly: true

A audiência não é decoração: é o que impede que um token de KSA projetado para outro serviço seja reaproveitado num enroll. O control-plane a verifica e recusa quem não bate — inclusive quem projeta sem audiência nenhuma.

Se o volume não estiver montado, o erro é explícito e diz isso:

RUNNER_ATTESTATION_MODE=gke-oidc: failed to read projected KSA token at
/var/run/secrets/tokens/gke-oidc/token (is the Workload Identity token volume mounted?)

Se a audiência estiver errada, o volume monta, o runner lê o token e o enroll é recusado — o erro chega como attestation_invalid, o mesmo que uma credencial ruim produziria. Antes de investigar a KSA ou o Workload Identity, confira o campo audience acima.

O aws-sts assina um GetCallerIdentity com a cadeia de credenciais default do SDK, que no EKS é o IRSA. Isso significa que a ServiceAccount anotada com o role IAM já basta, sem token de bootstrap e sem humano no navegador.

Ele exige AWS_REGION explicitamente. Sem ela o boot falha com RUNNER_ATTESTATION_MODE=aws-sts requires AWS_REGION. A região não é adivinhada de propósito, porque o control-plane só aceita região da própria allowlist e um palpite viraria um attestation_invalid que se lê como problema de credencial.

Prefira o secret manager que você já opera a Secrets do Kubernetes: RUNNER_SECRET_STORE_MODE=aws (AWS Secrets Manager) ou vault (HashiCorp Vault KV v2). Os dois resolvem por workload identity, sem chave estática. Veja Secret stores.

Para as credenciais de leitura da AWS, RUNNER_AWS_CRED_MODE=chain faz o SDK resolver por IRSA direto, sem passar chave nenhuma pelo cluster.

Só saída, nada de entrada. Se você usa NetworkPolicy, a lista de destinos está em Pré-requisitos. Lembre que a allowlist declarada nos manifests dos connectors é documentação, não enforcement: para os connectors nativos, quem aplica egress control é o cluster.

Sendo explícito sobre o gap, já que a pergunta vai aparecer: um chart de verdade precisaria de decisões que este repositório ainda não tomou: valores de resources (não há benchmark versionado) e a forma canônica de expor a escolha de atestação por provedor. (runAsNonRoot já não é um gap: a imagem roda como node/uid 1000 desde o hardening do #160.) Enquanto isso não existir, escrever os manifests do lado de quem instala é o caminho honesto.