K8s Debug Bridge
Redirige tráfico en vivo de un servicio de Kubernetes/OpenShift a una máquina local para depuración, usando solo kubectl/oc exec, el /dev/tcp integrado de bash y el nc dentro del pod bridge — sin necesidad de instalar CLI o agente adicional. Mecanismo: se crea un "pod bridge" temporal, el spec.selector del servicio destino se cambia temporalmente a la etiqueta de este pod, y un túnel inverso conecta el pod con el puerto local. Funciona en cualquier distribución de Kubernetes (vanilla K8s, OpenShift, EKS/GKE/AKS) — usa oc si está disponible, si no, kubectl (ver scripts/lib-kube-cli.sh). README.md cubre la justificación y las ventajas/desventajas frente a herramientas como Telepresence y mirrord.
Esto no está ligado a un solo lenguaje/framework: el bridge solo transporta tráfico. VS Code + Air + Delve (Go) es solo un ejemplo de integración — si trabajas en otro lenguaje, conecta tu propio watcher/depurador (Node: nodemon+inspector, Python: watchdog+debugpy, etc.) sobre el mismo túnel.
Si el lado local del usuario aún no está configurado — sin recargador, sin depurador adjunto, o si quiere que se adapte a su propio proyecto — lee docs/local-debug-setup.md en ese momento y síguelo. No reproduzcas su contenido de memoria, y no lo leas cuando la aplicación local ya se esté ejecutando y escuchando.
Esta habilidad crea un recurso real en tu clúster y modifica un recurso real: crea un Pod y cambia temporalmente el spec.selector de un Servicio. No son simulaciones — es estado real del clúster. Debe haber un plan de reversión en cada paso — nunca cambies el selector y te olvides de ello.
Prerrequisitos — verificar antes de preguntar al usuario
No le preguntes al usuario el nombre del namespace/servicio y lo pases ciegamente a bridge-up.sh — un nombre mal escrito fallará a mitad del script. Primero ejecuta scripts/check-env.sh [namespace] (solo lectura, no cambia nada):
- Ejecutar sin argumentos: muestra el estado de inicio de sesión y los namespaces accesibles. Si no has iniciado sesión, detente aquí y dile al usuario que ejecute
oc login/kubectl config use-context. - Ejecutar con un namespace: verifica que el namespace existe y lista los Servicios dentro de él (nombre + puerto + targetPort, incluidos los que ya están en bridge).
Muestra esta salida al usuario y deja que elija el Servicio destino de la lista — no le hagas adivinar. Luego:
- La imagen del pod bridge — siempre pregunta, nunca asumas. No elijas un valor por defecto y lo ejecutes: pregunta directamente al usuario "¿qué imagen debería usar el pod bridge?" y espera una respuesta. La imagen debe estar aprobada y ser accesible desde ese clúster específico, y solo ellos saben cuál es — muchos clústeres solo extraen de un registro interno aprobado, por lo que una imagen adivinada puede fallar al extraerse con el Servicio ya redirigido.
nicolaka/netshootes un ejemplo que se puede ofrecer, no un fallback para usar en silencio. Cualquiera que elijan debe incluirnc: el lado del pod usancen lugar de socat porque las imágenes endurecidas a menudo eliminansocatpero mantienen elncde Nmap.tunnel.shverifica la presencia dencantes de abrir el túnel y se detiene con un error claro si la imagen no lo tiene. - Manifiesto del pod: el pod bridge se renderiza desde
assets/bridge-pod.yaml. Si el clúster requiere imagePullSecrets, un securityContext, nodeSelector/tolerations o límites de recursos diferentes, edita ese archivo — no lo resuelvas en los scripts. Mantén la etiquetadebug: "__BRIDGE_NAME__"y el nombre del contenedordebug-bridge; los otros scripts se dirigen al pod a través de ellos. - El puerto: usa el
targetPortnumérico del Servicio de la salida decheck-env.sh, NO un puerto libre arbitrario. Si el Servicio usa un targetPort con nombre (ej.http), resuélvelo a su puerto de contenedor numérico. kube-proxy reenvía el tráfico altargetPortdel pod una vez que el selector se redirige — si eliges un número diferente, el tráfico nunca llega al pod bridge. - Dependencias salientes: este bridge solo maneja tráfico entrante (Clúster → Local). Si el código local debe llamar a otros servicios del clúster (
.svc.cluster.local), usa Routes/Ingress externos o port-forwards locales. - Secretos y entorno: nunca ejecutes
oc exec ... env,printenv,set, o un comando similar contra un pod de aplicación. Los valores pueden ser credenciales multilínea y se filtrarán en la salida del terminal/herramienta incluso cuando un comando parece seleccionar solo nombres. Nunca copies valores de entorno de un pod en un comando local, historial de shell o.env. Requiere un flujo de trabajo de gestión de secretos local preexistente o haz que el usuario proporcione el archivo de entorno local necesario fuera de banda. Verifica solo que la aplicación local se inicie; no imprimas su entorno.
La aplicación local ya debe estar ejecutándose antes de tocar el Servicio. Esto es una condición previa, no un paso posterior: el túnel entrega tráfico a 127.0.0.1:<puerto>, por lo que si nada escucha allí cuando el selector cambia, cada solicitud interceptada falla con connection-refused/502 mientras los pods reales ya están fuera de rotación. Haz que el usuario inicie su aplicación (con un archivo de entorno local provisionado de forma segura — nunca uno leído de un pod del clúster) y confirma el listener inmediatamente antes del paso 2:
lsof -nP -iTCP:<puerto> -sTCP:LISTEN
Flujo
-
Configurar el bridge: Antes de ejecutar nada, dile claramente al usuario: "Crearé un Pod real llamado
<bridge-pod>en el namespace<namespace>(imagen:<image>) — consume recursos del clúster y se eliminará cuando termines." Después de obtener aprobación, ejecutascripts/bridge-up.sh <namespace> <service> <bridge-pod> <image> [port]— esto crea el pod bridge y guarda el selector original tanto en estado local como en una anotaciónk8s-debug-bridge/owneren el Servicio (no requiere python3/jq, bash simple). No te saltes este paso — nunca redirijas sin haber guardado el selector original, o no habrá forma de volver atrás. Si el servicio ya está en bridge por otra persona, este script falla y se detiene (verificación de anotación) — en ese caso, muestra al usuario el propietario del mensaje de error, no lo intentes tú mismo. -
Redirigir el servicio: Antes de cambiar el selector del Servicio destino por la etiqueta del pod bridge, dile claramente al usuario y obtén aprobación real: "A partir de este momento, TODO el tráfico a
<service>irá a tu pod bridge — los pods reales dejarán de recibir tráfico." Después de la aprobación, ejecutascripts/redirect-service.sh <namespace> <service> --yes—--yesomite el prompt interactivoreaddel script (ya que obtuviste aprobación en el chat); sin--yesel script espera en stdin y puede colgarse. El script reemplaza el mapa de selector completo solo condebug=<bridge-pod>; no debe fusionar esta etiqueta en el selector original, porque el pod bridge no lleva las etiquetas originales de la aplicación. -
Iniciar el túnel:
scripts/tunnel.sh <namespace> <service> <bridge-pod> <port>— este script se bloquea, por lo que debe ejecutarse en su propio terminal o en un proceso en segundo plano. Se autolimpia: eltrapcaptura EXIT/INT/TERM, por lo que Ctrl+C o cualquier otra terminación llama automáticamente acleanup.sh. Si lo iniciaste en segundo plano tú mismo, no hay Ctrl+C que presionar — detenlo conkill -TERM <pid>, que el trap también maneja; anota el PID cuando lo lances y dile al usuario cuál es. Ejecutarcleanup.shmanualmente solo es necesario si el proceso se mata de forma inatrapable (kill -9). Transporta una conexión a la vez y crea automáticamente un nuevo listener del lado del pod después de que cada conexión se cierra, así que deja el mismo túnel funcionando entre solicitudes de depuración secuenciales. Si el túnel registra repetidamenteWARNING: could not carry a connection, nada está escuchando en127.0.0.1:<puerto>— la aplicación local está caída, aún compilando, o una recarga en caliente liberó el puerto. Arregla eso en lugar de reiniciar el túnel. -
Depuración local: el puerto que puenteas es el puerto en el que sirve la aplicación (el
targetPortdel Servicio), nunca el puerto en el que escucha su depurador. Si el usuario dice "mi depurador está en 2345, puentea eso", está confundiendo los dos: corrígelo, pregúntale en qué puerto sirve la aplicación misma, y puentea ese. El tráfico enviado a un puerto de depurador devuelve 502 que parecen un túnel roto. La aplicación local ya se está ejecutando y verificada (ver la condición previa anterior); es la que recibe el tráfico del túnel. El depurador de su lenguaje (ej. Delve, un inspector de Node) se adjunta a esa aplicación en ejecución por separado, generalmente en un puerto diferente — ej. en Go, Air + Delve en el puerto 2345 mientras la aplicación misma escucha en el<puerto>del túnel. Una recarga en caliente puede eliminar brevemente el listener; mientras está ausente, el bridge devuelve fallos de conexión/502. Si la aplicación local no se recupera rápidamente, detén el túnel y limpia el Servicio en lugar de dejar todo el tráfico apuntando a un bridge fallido. El bridge transporta la ruta HTTP sin cambios: configura el prefijo base de la aplicación local para que coincida con la ruta entregada al Servicio; no reproduce una reescritura de Ingress. El tráfico puede venir de cualquier llamante dentro del clúster del Servicio (por ejemplo, un proxy Nginx de panel); no se requiere una Route/Ingress pública. Si la ejecución se pausa más allá del tiempo de espera del llamante, reanúdala y dispara una nueva solicitud. Después de que una recarga en caliente reinicie Delve, el IDE puede necesitar adjuntarse de nuevo. Cuando Delve se inició con--accept-multiclient, el agente también puede adjuntar un cliente Delve de terminal al puerto2345junto a VS Code para inspeccionar la línea de código actual, la pila y las variables. Informa las observaciones al usuario; obtén aprobación explícita antes de continuar la ejecución o cambiar el estado del depurador. -
Al terminar: Ctrl+C en el terminal del túnel, o
kill -TERM <pid>si lo iniciaste en segundo plano — el selector del servicio y el pod bridge se revierten automáticamente, incluida la restauración del mapa de selector original completo. Si el trap no se disparó (ej.kill -9),scripts/cleanup.sh <namespace> <service>debe ejecutarse manualmente; recuérdale al usuario esto antes de que termine la sesión.
Nota de seguridad
Este método no instala ningún componente permanente en el clúster (sin webhook mutante, inyector de sidecar o RBAC a nivel de clúster) — solo un pod temporal claramente marcado por su nombre/etiqueta (debug=<nombre-pod-bridge>) y permisos exec estándar. Aun así, hay dos riesgos reales, advierte al usuario sobre ellos:
- Olvidar revertir el selector del servicio — una interrupción permanente en prod/stage.
- El permiso
execpuede ser abusado a través de RBAC — esta habilidad solo debe ejecutarse en tu propio namespace, en tu propio pod bridge, y nunca debería necesitar cluster-admin.
Para pasos detallados y solución de problemas, consulta README.md.
Antes de finalizar la sesión, verifica si queda un bridge sin limpiar. El estado reside junto a scripts/, en la propia carpeta de esta habilidad — no en el directorio del proyecto del usuario. Resuélvelo relativo a los scripts que estás ejecutando, por ejemplo:
find "$(dirname <ruta-a>/scripts)/.state" -name '*.env'
Ejecutar un simple find .state desde el proyecto del usuario encontrará silenciosamente nada y reportará un estado limpio incluso cuando un bridge sigue activo. Si existe un archivo .env allí, dile al usuario qué namespace/servicio nombra y sugiere scripts/cleanup.sh <namespace> <service>. Esto es un recordatorio, no una compuerta determinista — ten cuidado de no olvidarlo.