K8s Debug Bridge
将 Kubernetes/OpenShift 服务的实时流量隧道转发到本地机器,用于调试。仅使用标准的 kubectl/oc exec、bash 内置的 /dev/tcp 以及桥接 Pod 内部的 nc——无需安装额外的 CLI 或代理。工作原理:启动一个临时的“桥接 Pod”,将目标服务的 spec.selector 临时切换到该 Pod 的标签,并通过反向隧道在 Pod 和本地端口之间建立连接。适用于任何 Kubernetes 发行版(标准 K8s、OpenShift、EKS/GKE/AKS)——如果可用则使用 oc,否则使用 kubectl(参见 scripts/lib-kube-cli.sh)。README.md 涵盖了设计原理以及与 Telepresence、mirrord 等工具相比的优缺点。
此技能不绑定到特定的语言/框架:桥接仅传输流量。VS Code + Air + Delve(Go)只是一个示例集成——如果你使用其他语言,请通过同一隧道连接你自己的监视器/调试器(Node:nodemon+inspector,Python:watchdog+debugpy 等)。
如果用户本地端尚未设置完成——没有重载器、没有附加调试器,或者他们希望适配到自己的项目——请阅读 docs/local-debug-setup.md 并按照其中的步骤操作。不要凭记忆复述其内容,当本地应用程序已在运行并处于监听状态时也不要阅读该文件。
此技能会在集群中创建真实的资源并修改真实的资源:它会创建一个 Pod 并临时更改服务的 spec.selector。这些不是模拟——这是真实的集群状态。每一步都必须有回滚计划——切勿更改选择器后就忘记恢复。
先决条件——在询问用户之前进行验证
不要直接询问用户命名空间/服务名称然后盲目地传给 bridge-up.sh——拼写错误的名称会导致脚本执行中途失败。首先运行 scripts/check-env.sh [namespace](只读,不更改任何内容):
- 不带参数运行:显示登录状态和可访问的命名空间。如果未登录,则在此停止并告知用户运行
oc login/kubectl config use-context。 - 带命名空间运行:验证命名空间是否存在,并列出其中的服务(名称 + 端口 + targetPort,包括已经桥接的服务)。
向用户显示此输出,让他们从列表中选择目标服务——不要让他们猜测。然后:
- 桥接 Pod 镜像——始终询问,切勿假设。 不要选择一个默认镜像然后直接运行:直接询问用户“桥接 Pod 应该使用哪个镜像?”并等待回答。该镜像必须经过该特定集群的批准并且可以从该集群访问,只有用户知道哪一个可以选择——许多集群只从批准的内部仓库拉取镜像,因此猜测的镜像可能在拉取时失败,而此时服务已经被重定向。
nicolaka/netshoot是一个可以提供的示例,而不是默默使用的备用方案。无论用户选择什么,都必须包含nc:Pod 端使用nc而不是 socat,因为加固镜像通常移除socat而保留 Nmap 的nc。tunnel.sh会在打开隧道之前检查nc,如果镜像中没有则停止并给出清晰的错误信息。 - Pod 清单:桥接 Pod 从
assets/bridge-pod.yaml渲染生成。如果集群需要 imagePullSecrets、securityContext、nodeSelector/tolerations 或不同的资源限制,请编辑该文件——不要在脚本中绕过。保留debug: "__BRIDGE_NAME__"标签和debug-bridge容器名称;其他脚本通过它们来寻址该 Pod。 - 端口:使用
check-env.sh输出中服务的数字targetPort,而不是任意的空闲端口。如果服务使用命名的 targetPort(例如http),请将其解析为对应的数字容器端口。一旦选择器被重定向,kube-proxy 会将流量转发到 Pod 的targetPort——如果你选择了不同的数字,流量将永远不会到达桥接 Pod。 - 出站依赖:此桥接仅处理入站流量(集群 → 本地)。如果本地代码必须调用其他集群服务(
.svc.cluster.local),请使用外部 Route/Ingress 或本地端口转发。 - 密钥和环境变量:切勿对应用 Pod 运行
oc exec ... env、printenv、set或类似命令。这些值可能是多行凭据,即使命令看似只选择名称,也会泄漏到终端/工具输出中。切勿将 Pod 环境变量值复制到本地命令、shell 历史或.env文件中。需要预先存在的本地密钥管理工作流,或让用户通过带外方式提供所需的本地环境文件。仅验证本地应用程序是否启动;不要打印其环境。
在触及服务之前,本地应用程序必须已经运行。 这是前提条件,而不是后续步骤:隧道将流量传递到 127.0.0.1:<port>,因此如果选择器切换时没有程序在监听该端口,那么每个被拦截的请求都会失败并返回 connection-refused/502,而此时真实 Pod 已退出轮换。让用户启动他们的应用程序(使用安全配置的本地环境文件——绝不可从集群 Pod 中读取),并在步骤 2 之前立即确认监听器:
lsof -nP -iTCP:<port> -sTCP:LISTEN
流程
-
设置桥接:运行任何操作之前,明确告知用户:“我将在
<namespace>命名空间中创建一个名为<bridge-pod>的真实 Pod(镜像:<image>)——它会消耗集群资源,并在你完成后被删除。” 获得批准后,运行scripts/bridge-up.sh <namespace> <service> <bridge-pod> <image> [port]——此脚本创建桥接 Pod,并将原始选择器同时保存到本地状态以及作为k8s-debug-bridge/owner注释保存在服务上(无需 python3/jq,纯 Bash)。不要跳过此步骤——切勿在没有保存原始选择器的情况下进行重定向,否则将无法回滚。如果服务已被其他人桥接,此脚本将失败并停止(通过注释检查)——在这种情况下,向用户显示错误消息中的所有者,不要自行重试。 -
重定向服务:在将目标服务的选择器切换到桥接 Pod 的标签之前,明确告知用户并获得真实批准:“从此刻起,所有发往
<service>的流量都将被路由到你的桥接 Pod——真实 Pod 将停止接收流量。” 批准后,运行scripts/redirect-service.sh <namespace> <service> --yes——--yes跳过脚本自身的交互式read提示(因为你在聊天中已经获得了批准);不带--yes时脚本会等待 stdin 输入,可能导致挂起。该脚本将完整的选择器映射替换为仅含debug=<bridge-pod>;它绝不能将此标签合并到原始选择器中,因为桥接 Pod 没有应用原来的标签。 -
启动隧道:
scripts/tunnel.sh <namespace> <service> <bridge-pod> <port>——此脚本会阻塞,因此应在单独的终端或后台进程中运行。它会自动清理:trap捕获 EXIT/INT/TERM,因此 Ctrl+C 或任何其他终止操作都会自动调用cleanup.sh。如果你自己在后台启动了它,则无法按 Ctrl+C——使用kill -TERM <pid>停止它,trap 也能处理此信号;启动时记录 PID 并告知用户。仅在进程被不可捕获方式终止(kill -9)时才需要手动运行cleanup.sh。 该隧道每次处理一个连接,并在每次连接关闭后自动创建新的 Pod 端监听器,因此在连续的调试请求之间保持同一隧道运行。 如果隧道反复记录WARNING: could not carry a connection,则说明127.0.0.1:<port>上没有程序在监听——本地应用程序已关闭、仍在构建中,或者热重载释放了端口。请修复该问题,而不是重新启动隧道。 -
本地调试:你桥接的端口是应用提供服务的端口(服务的
targetPort),绝不是其调试器监听的端口。如果用户说“我的调试器在 2345 端口,桥接那个”,则他们混淆了两者:纠正他们,询问应用本身在哪个端口提供服务,并桥接那个端口。发送到调试器端口的流量会返回 502,看起来像是隧道故障。 本地应用程序已经运行并通过验证(参见上面的前提条件);它是接收隧道流量的程序。他们语言的调试器(例如 Delve、Node inspector)通过另一个端口(例如在 Go 中,Air + Delve 在 2345 端口,而应用本身在桥接的<port>上监听)分别附加到正在运行的应用程序。 热重载可能会短暂移除监听器;在此期间桥接会返回连接失败/502。如果本地应用程序未及时恢复,请停止隧道并清理服务,而不是让所有流量指向故障的桥接。桥接原样传输 HTTP 路径:配置本地应用的基础前缀以匹配传递给服务的路径;它不会复制 Ingress 的重写规则。 流量可能来自集群中任意调用该服务的组件(例如,仪表盘 Nginx 代理);不需要公共 Route/Ingress。如果执行暂停时间超过调用方的超时,请恢复执行并触发新请求。热重载重新启动 Delve 后,IDE 可能需要重新附加。 当 Delve 使用--accept-multiclient启动时,代理可能还会在 2345 端口附加一个终端 Delve 客户端,与 VS Code 一起查看当前源代码行、堆栈和变量。将观察到的信息报告给用户;在继续执行或以其他方式更改调试器状态之前,必须获得明确批准。 -
完成后:在隧道终端中按 Ctrl+C,或者如果你在后台启动则使用
kill -TERM <pid>——服务选择器和桥接 Pod 自动恢复,包括恢复完整的原始选择器映射。如果 trap 未触发(例如kill -9),必须手动运行scripts/cleanup.sh <namespace> <service>;在用户结束会话之前提醒他们这一点。
安全说明
此方法不会在集群中安装永久组件(无 mutating webhook、sidecar 注入器或集群范围的 RBAC)——只有一个临时的 Pod,通过其名称/标签(debug=<bridge-pod-name>)明确标记,以及标准的 exec 权限。即便如此,存在两个真实风险,请警告用户:
- 忘记恢复服务选择器——导致生产/预发布环境永久中断。
exec权限可能被 RBAC 滥用——此技能只应在你自己的命名空间中,针对你自己的桥接 Pod 运行,且绝不需要集群管理员权限。
有关详细步骤和故障排除,请参见 README.md。
在结束会话之前,检查是否有未清理的桥接残留。状态文件位于 scripts/ 旁边,即此技能自己的文件夹中——不在用户的项目目录中。 根据你正在运行的脚本解析其相对路径,例如:
find "$(dirname <path-to>/scripts)/.state" -name '*.env'
从用户项目目录中运行裸 find .state 将静默地找不到任何内容并报告干净状态,即使桥接仍然存在。如果那里存在 .env 文件,请告知用户该文件引用的命名空间/服务名称,并建议运行 scripts/cleanup.sh <namespace> <service>。这是一个提醒,而不是确定性门禁——注意不要忘记。