Kubernetes
A base Kustomize do Agent está publicada junto desta documentação, uma por versão: Namespace,
ConfigMap, StatefulSet com um volume de identidade por Agent, probes exec e NetworkPolicy. Ela não traz
valor de ambiente nenhum: a imagem, as credenciais e a StorageClass entram por um overlay seu. Não há Helm
chart.
Os requisitos mais abaixo valem para a base e para quem preferir escrever os próprios manifests.
Instalação
Seção intitulada “Instalação”Baixe a base da mesma versão da imagem que você roda:
VERSAO=X.Y.Z # a tag da sua imagem, com ou sem o "v"BASE="https://docs.whystack.ai/kubernetes/v${VERSAO#v}/base"mkdir -p baseARQUIVOS=$(curl -fsSL "$BASE/files.txt") && for f in $ARQUIVOS; do curl -fsSL -o "base/$f" "$BASE/$f"; doneA pasta publicada leva o v da tag do release (/kubernetes/v1.29.0/base/); a tag da imagem não
(whystack-agent:1.29.0). O ${VERSAO#v} aceita as duas formas. Se a versão não tiver base publicada,
o primeiro curl falha com 404 e nada é baixado.
Ao lado dela, crie o overlay com a imagem que você recebeu, por registry e digest (ver Imagem):
apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationresources: - baseimages: - name: whystack-agent newName: <endereço>/whystack-agent digest: sha256:<digest>Crie o Secret e aplique:
kubectl apply -f base/namespace.yamlkubectl -n whystack-agent create secret generic whystack-agent-env --from-env-file=agent.envkubectl apply -k .kubectl -n whystack-agent rollout status statefulset/whystack-agent --timeout=5mO agent.env leva o RUNNER_BOOTSTRAP_TOKEN e, com RUNNER_SECRET_STORE_MODE=env (o default da base),
as credenciais dos connectors. Depois do primeiro hello accepted, remova o RUNNER_BOOTSTRAP_TOKEN do
Secret: ele foi consumido, e os restarts renovam a identidade a partir do volume.
Se a identidade se perder (o PVC foi apagado, ou ela expirou além da graça de renovação, ver
Quando a identidade se perde), o boot falha com
RUNNER_ATTESTATION_MODE=bootstrap-token requires RUNNER_BOOTSTRAP_TOKEN e o pod entra em
CrashLoopBackOff. O token antigo não volta a valer: minte um novo em Onboarding, recoloque-o no
Secret e recrie o pod (kubectl -n whystack-agent delete pod whystack-agent-0), porque o Secret só é
lido quando o contêiner nasce.
Mudar a configuração
Seção intitulada “Mudar a configuração”A configuração não secreta da base sai de um configMapGenerator. Para mudar uma chave, faça o merge no
seu overlay; o nome do ConfigMap ganha o hash do conteúdo, e o kubectl apply -k . recria o pod com o
valor novo:
# kustomization.yaml (o mesmo overlay de cima)configMapGenerator: - name: whystack-agent-config behavior: merge literals: - RUNNER_SECRET_STORE_MODE=aws - RUNNER_SECRETS_MANAGER_REGION=us-east-1Mais de um Agent
Seção intitulada “Mais de um Agent”N Agents são N instalações, não N réplicas. O token de bootstrap é single-use e o Secret é um só por
instalação, então escalar o StatefulSet faz o segundo pod falhar no enrollment. Cada Agent ganha um overlay
com o próprio namespace (o Kustomize renomeia também o Namespace da base), o próprio Secret e o próprio
token:
apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationnamespace: whystack-agent-bresources: - ../baseimages: - name: whystack-agent newName: <endereço>/whystack-agent digest: sha256:<digest>kubectl create namespace whystack-agent-bkubectl -n whystack-agent-b create secret generic whystack-agent-env --from-env-file=agent-b.envkubectl apply -k agent-bkubectl -n whystack-agent-b rollout status statefulset/whystack-agent --timeout=5mO agent-b.env leva um bootstrap token novo, mintado para este Agent: o do primeiro já foi consumido.
O RUNNER_INSTANCE_ID da base é <pod>.<namespace>.<uid do pod>: único também entre clusters, então
Agents em clusters diferentes com o mesmo namespace aparecem separados na Frota. O UID muda quando o pod
é recriado: a entrada antiga aparece desconectada e sai da Frota sozinha.
O contêiner
Seção intitulada “O contêiner”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 @whystack/otel/register apps/runner/dist/index.jsA 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.
Requisito 1: sinal de saúde por exec, não por HTTP
Seção intitulada “Requisito 1: sinal de saúde por exec, não por HTTP”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. Não configure httpGet em probe nenhum: todos vão falhar.
O sinal de saúde é um arquivo. O runner grava em RUNNER_HEALTH_DIR (default /tmp/whystack-health)
e a imagem traz o whystack-health, que o lê de dentro do contêiner:
whystack-health live: o processo está vivo e o event loop anda (batimento com menos de 60 s);whystack-health ready: vivo e enrollado, com o túnel conectado e ohelloaceito.
livenessProbe: exec: command: [whystack-health, live] periodSeconds: 20 timeoutSeconds: 5 readinessProbe: exec: command: [whystack-health, ready] periodSeconds: 10 timeoutSeconds: 5Nunca use o ready como liveness. O túnel cai quando a rede cai, e quem reconecta é o backoff do
runner: reiniciar o pod por isso troca um runner que reconecta sozinho por um que recomeça do zero.
Com readOnlyRootFilesystem: true, monte um emptyDir em /tmp (ou aponte RUNNER_HEALTH_DIR para um
volume gravável pelo uid 1000). Sem onde gravar, o probe falha, que é a forma honesta de dizer que o
sinal não existe. A saúde por connector continua no handshake e na Frota do control-plane.
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 folgaUm 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.
Requisito 3: identidade em volume, cache em memória
Seção intitulada “Requisito 3: identidade em volume, cache em memória”A identidade mTLS persiste em RUNNER_IDENTITY_DIR: um restart renova o certificado a partir dela em
vez de gastar outro bootstrap token. Em Kubernetes isso é um PVC por Agent: o StatefulSet da base
cria um por ordinal, gravável pelo uid 1000 (fsGroup: 1000). Nunca compartilhe o volume entre Agents,
porque cada boot sobrescreveria a identidade do outro.
O cache do learned-store é outra coisa e segue :memory: por default: o que persiste é identidade, não
estado, e a fonte de verdade do que foi aprendido é o control-plane. Não aponte
RUNNER_LEARNED_STORE_PATH para o volume da identidade.
Com atestação de plataforma (gke-oidc, aws-sts), o Agent re-atesta a cada boot e o volume é
dispensável. Alguns provedores têm disco mínimo bem maior que o necessário (a MGC e o GCE exigem 10 GiB,
mesmo que a identidade ocupe poucos KiB), e a base já pede esse tamanho. Confira o reclaimPolicy da
sua StorageClass: com Retain, apagar o PVC não apaga o disco.
Requisito 4: identidade sem segredo na imagem
Seção intitulada “Requisito 4: identidade sem segredo na imagem”Este é o ponto em que Kubernetes muda o desenho, e para melhor.
GKE: gke-oidc
Seção intitulada “GKE: gke-oidc”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 whystack-enroll. Sobre a base, isso é um patch
no overlay; o contêiner da base se chama agent, e o Kustomize casa contêineres pelo nome:
apiVersion: apps/v1kind: StatefulSetmetadata: name: whystack-agentspec: template: spec: volumes: - name: whystack-token projected: sources: - serviceAccountToken: path: token audience: whystack-enroll # o control-plane exige esta audiência expirationSeconds: 3600 containers: - name: agent volumeMounts: - name: whystack-token mountPath: /var/run/secrets/tokens/gke-oidc readOnly: true# kustomization.yaml (o overlay)patches: - path: patch-gke-oidc.yamlconfigMapGenerator: - name: whystack-agent-config behavior: merge literals: - RUNNER_ATTESTATION_MODE=gke-oidcA 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.
EKS: aws-sts, e não eks-oidc
Seção intitulada “EKS: aws-sts, e não eks-oidc”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.
Requisito 5: segredos
Seção intitulada “Requisito 5: segredos”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). O aws
resolve pela cadeia de workload identity, sem chave estática. O vault autentica por token ou AppRole:
o token da ServiceAccount do pod ainda não é um método de login no Vault. Veja
Secret stores. A base usa env, com o Secret whystack-agent-env.
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.
Requisito 6: egress
Seção intitulada “Requisito 6: egress”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.
Helm e dimensionamento
Seção intitulada “Helm e dimensionamento”Não há Helm chart. Os resources da base são ponto de partida, não benchmark: meça CPU e memória com os
seus connectors reais antes de apertar os limites. A escolha de atestação por provedor também fica com o
seu overlay (ver Mudar a configuração): a base usa bootstrap-token, que
funciona em qualquer cluster.