Solución de problemas
La imagen no se descarga
Sección titulada «La imagen no se descarga»no basic auth credentials o denied en docker pull
El registry es privado y todavía no te autenticaste, o la identidad que hace el pull no es la que autorizamos. Consulta Autenticarse antes del pull.
Si el pull falla dentro del orquestador pero funciona desde tu terminal, la identidad es otra: ahí quien hace el pull es la plataforma (task execution role, role del node group, service account), no tú. Dinos cuál es para que podamos autorizarla.
El runner no se conecta
Sección titulada «El runner no se conecta»certificate has expired
El certificado de tu tenant venció. No se renueva solo: pide un par nuevo a WhyStack, reemplaza
el contenido montado en /certs y recicla las réplicas.
Para que no te vuelva a pasar, el notAfter sale en el log en cada boot:
{"level":"info","message":"using provisioned cert","context":"runner","notAfter":"..."}is not in the cert's altnames
Te conectaste por un hostname que no está en el SAN del certificado del túnel. Aunque resuelva a la
IP correcta, TLS lo rechaza. En producción el hostname es tunnel.whystack.ai.
No pasa nada, y no hay error
Confirma que hay un túnel de control-plane escuchando en RUNNER_TUNNEL_URL. En desarrollo el valor
por defecto es :8443 en el host; en Linux, Compose ya mapea host.docker.internal vía
host-gateway.
Un ciclo 1006 → exited 0 → restart
El túnel del otro lado se está reiniciando. Estabilízalo y el runner deja de ciclar. El exited 0 se
debe a que el timer de reconexión está marcado con unref; restart: unless-stopped lo trae de
vuelta cuando el túnel vuelve.
El boot falla de inmediato
Sección titulada «El boot falla de inmediato»El runner valida la configuración por schema y muere con el motivo. Los más comunes:
| Mensaje | Qué hacer |
|---|---|
either provide RUNNER_CERT_PEM+RUNNER_KEY_PEM (provisioned cert) or RUNNER_ENROLL_URL |
Elige uno de los dos caminos de identidad. |
RUNNER_ATTESTATION_MODE=bootstrap-token requires RUNNER_BOOTSTRAP_TOKEN |
Falta el token. El fail-fast es intencional: sin él, el síntoma se vuelve un loop de 401 que parece un problema de red. |
RUNNER_ATTESTATION_MODE=aws-sts requires AWS_REGION |
La región no se adivina, a propósito. |
RUNNER_SECRET_STORE_MODE=vault requires RUNNER_VAULT_ADDR |
Configuración de Vault incompleta. |
attestation mode 'eks-oidc' not implemented |
eks-oidc no existe. En EKS, usa aws-sts. |
op "…" claimed by both datadog and grafana |
Los dos providers de observabilidad están registrados. Elige uno en RUNNER_OBSERVABILITY. |
Could not locate the bindings file |
La imagen se construyó con --ignore-scripts. better-sqlite3 es un addon nativo y necesita su postinstall. |
Una tool devuelve la forma equivocada
Sección titulada «Una tool devuelve la forma equivocada»Síntomas típicos: entries.map is not a function, u “op no implementada”.
Estás en synthetic, que es el valor por defecto y es solo un seed de 3 operaciones cuyas formas
no coinciden con las de los connectors reales.
- Para datos reales:
RUNNER_CONNECTORS=realmás las claves. - Para el catálogo entero sin credenciales:
RUNNER_CONNECTORS=demo.
Un connector desapareció
Sección titulada «Un connector desapareció»La señal está en el log de boot:
connector not served {connectorId: "azion", missing: ["AZION_TOKEN"]}Tres causas, en orden de frecuencia:
- La clave nunca llegó al contenedor. El pass-through de Compose es por nombre y explícito: una
variable ausente en el host es una clave omitida en el contenedor. Verifica que el nombre esté
en la lista del
docker-compose.yml. - El nombre es distinto. El connector
aziondeclaraAZION_TOKEN; un vault que guardaAZION_API_TOKENproduce exactamente el síntoma de clave ausente. Verifica los nombres exactos en Credenciales por connector. - El secreto no está en el store. En los backends remotos, “no encontrado” se resuelve como
undefineden vez de una excepción, a propósito, para que el modelo lazy funcione.
El health check de EKS dice skipped
Sección titulada «El health check de EKS dice skipped»skipped: no cluster configured (EKS_CLUSTER)Falta EKS_CLUSTER. Vale la pena distinguirlo: el connector aparece como no configurado en la
Flota, que es distinto de roto, e igual de ciego.
El health check de EKS dice EKS cluster não encontrado nesta conta
Sección titulada «El health check de EKS dice EKS cluster não encontrado nesta conta»EKS cluster não encontrado nesta conta: orders-cluster (região us-east-1; clusters ali: data-cluster)La credencial funciona: el Agent llegó a la API de AWS, y el clúster que nombra EKS_CLUSTER no
existe en la cuenta y la región que lee. EKS_CLUSTER es por Agent, y cada Agent lee una sola
cuenta. El caso típico es una flota con un Agent por cuenta y el mismo valor en todos, donde solo el
Agent de la cuenta correcta encuentra el clúster.
Rotar el secreto no ayuda. Apunta cada Agent a un clúster de su propia cuenta (el mensaje lista los
que existen ahí), o quita la variable del Agent cuya cuenta no tiene clúster, y el probe vuelve a
skipped, que en ese caso es el estado verdadero. Sin el permiso eks:ListClusters, el mensaje sale
sin la lista.
Activé OTel y no veo spans
Sección titulada «Activé OTel y no veo spans»El interruptor maestro por sí solo no alcanza: el exporter por defecto es none. Consulta
Telemetría.
Por qué un campo vuelve null
Sección titulada «Por qué un campo vuelve null»Esta es la pregunta de soporte más frecuente, y la respuesta importa: null aquí significa “no
sé”, nunca “es cero”. La distinción es deliberada, y salió de una clase de incidente en la que un
0 fabricado se leyó como salud.
Tres reglas que sigue el edge:
1. Antes de devolver null, intenta la otra fuente. Tener la tasa de error no termina la lectura
de latencia: si los percentiles no llegan listos y hay spans crudos disponibles, se calculan a partir
de ellos, y el resultado dice de dónde salió (percentileSource: 'spans').
2. Un null que sobrevive lleva el código del motivo. Campos como percentilesUnavailable e
instancesUnavailable existen para que “no se emitió” y “no consulté” dejen de verse idénticos.
3. Un payload vacío prueba si la pregunta existía. Un nombre de servicio que no coincide con
ningún servicio de APM vuelve con serviceResolved: false y una lista de candidatos, en vez del mismo
null generalizado que produciría un servicio real y silencioso.
Y una métrica derivada de un denominador cero es null, nunca 0: una razón sin denominador es
indefinida, y 0% es lo opuesto de “no sé”: lleva a la acción opuesta.
Un log de error contiene [INFO]
Sección titulada «Un log de error contiene [INFO]»No es un bug. Datadog envía todo stderr a status:error, así que una búsqueda de errores puede
devolver líneas cuyo texto dice otra cosa.
El borde expone la divergencia con textLevel y levelMismatch, en vez de reescribir level.
La clasificación de la fuente es un hecho sobre la fuente, y corregirla en silencio borraría
información.
El runner murió y no volvió
Sección titulada «El runner murió y no volvió»Si el certificado pasó 6 minutos de su vencimiento (5 de gracia del control-plane, 1 de margen para skew), la identidad es irrecuperable: la atestación de renovación prueba la posesión con el certificado que murió.
El runner sale del proceso a propósito, para que el supervisor levante un reemplazo que hace el enrollment desde cero. Confirma que tu supervisor esté configurado para eso. Sin eso, el efecto es un runner que desaparece.
El mensaje del lado del control-plane:
presented cert expired beyond the renewal grace windowDel lado del Agent, la misma situación aparece así, y la segunda línea solo existe cuando el host dejó de contar el tiempo (suspensión, snapshot, un contenedor sin CPU):
{"level":"error","message":"identity expired beyond the renewal grace window — pausing reconnects"}{"level":"warn","message":"renewal deadline slipped — wall-clock advanced while this process was not running"}Mientras tanto el Agent deja de reconectarse, en vez de repetir un handshake que ya sabe que será
rechazado. Las caídas de ese período se etiquetan como identity_expired, no network_error: un
certificado vencido cierra el socket sin aviso, lo que parece una caída de red y te manda a
investigar el lado equivocado.
Perdí estado aprendido después de un kill
Sección titulada «Perdí estado aprendido después de un kill»Es lo esperado en un reclaim duro (SIGKILL, OOM, spot sin drain). El flush periódico
(RUNNER_LEARNED_FLUSH_INTERVAL_MS, por defecto 60 s) acota la pérdida; no la elimina.
Recuerda también que el learned-store local es una caché: la fuente de verdad es el
control-plane. En Kubernetes, dimensiona terminationGracePeriodSeconds por encima de
RUNNER_DRAIN_TIMEOUT_MS (por defecto 120 s) para que el drain pueda completarse.
Cómo observar un runner que no expone ningún puerto
Sección titulada «Cómo observar un runner que no expone ningún puerto»No hay probe HTTP, porque el runner es un cliente de salida. Las señales disponibles son:
whystack-health, desde dentro del contenedor:live(el proceso está corriendo) yready(enrollment hecho, túnel conectado,helloaceptado). Es lo que usan elHEALTHCHECKde la imagen y los probesexecde Kubernetes, y su mensaje de falla dice qué condición falta.- Logs de boot:
boot: liveness,boot: readyy cualquierconnector not served. - La vista Flota del control-plane, que ve el runner conectado y el estado por connector reportado en el handshake.
- OTel, si está activado correctamente.