我最近在 k8s_infra/plugins/infra-observer 做了一個給 AI 用的 MCP tool。它不是遠端終端機的包裝,也不是把 kubectl 直接交給模型;它只做一件事:在不讓 AI 動到環境的前提下,給它排查問題需要的資訊。
起點其實很單純。讓 AI SSH 到主機後,若工具可以傳任意指令,等於把太多決定權交了出去。就算名義上說是 read-only,也可能讀到 Secret、進 Pod 執行命令,或被 log 裡的內容引導去跑不該跑的東西。我想要的是讓它看得到 Pod 是否不健康、rollout 卡在哪裡、Argo CD 有沒有同步、Service 有沒有 endpoint,以及服務在 OTel 裡有沒有錯誤;除此之外的能力就不要有。
工具介面先縮小
infra-observer 沒有 shell、generic SSH 或 generic kubectl。它把常用的排查動作做成具名工具,例如 health summary、unhealthy Pods、rollout status、events、bounded Pod logs、Argo CD 狀態、Service endpoints、Docker status,以及某個服務的 OTel error 摘要。
這樣做看起來不如「給一個 shell」方便,但輸入會變得很清楚。namespace、workload 和 service name 都要是合法的 Kubernetes 名稱;log 行數、查詢筆數、輸出大小和 timeout 都有限制。本機端直接組 argv,不經過 shell。Secret read、Pod exec、port-forward、變更資源、任意 URL,以及任意 PromQL、LogQL、TraceQL 都沒有對應的工具可以呼叫。
Kubernetes 與 observability 是兩個不同來源,但處理方式相同。Kubernetes 端用獨立的 infra-observer 身分和最小 RBAC;工具也會用 kubectl auth can-i 確認 Secret、exec、mutation 與 impersonation 真的被拒絕。OTel 端則只檢查固定的 Grafana、Loki、Tempo、Prometheus、Vault 和 Collector endpoint,並針對一個已驗證的 service name 查 error 相關資料。這樣 AI 拿到的是排查線索,不是一張可以逛完整個平台的通行證。
SSH 端也不相信 client
本機 allowlist 不夠。假如 plugin 或 SSH 設定被繞過,遠端主機還是可能收到不受限的命令。所以我用了專用 SSH key,並在 authorized_keys 上指定 forced command。連線後不會得到 shell,而是先進 root-owned 的 forced_command.py。
這段是檔案裡 kubectl 規則的刪節版。它比對完整 argv,而不是只看指令開頭。可變的 namespace 還要經過 DNS label 驗證,輸出欄位也固定;沒有被寫進規則的命令就回傳 False。
def kubectl_allowed(args: list[str]) -> bool:
if tuple(args) in EXACT_KUBECTL_COMMANDS:
return True
if len(args) == 8 and args[:4] == ["kubectl", "get", "pods", "-n"]:
return is_name(args[4]) and args[5:] == [
"-o", POD_STATUS_COLUMNS, "--no-headers"
]
# 其他明確規則:workload、events、logs、endpoints 等。
return False
allowlist 通過後,wrapper 還會用固定的 binary 路徑、固定 kubeconfig 與乾淨的環境執行命令。它不繼承 client 的 HOME、PATH、KUBECONFIG、Docker 或 XDG 設定,也不開放 TTY、port forwarding、agent forwarding 或 X11。實際執行前的流程很短:解析 SSH_ORIGINAL_COMMAND、拒絕不符合規則的 argv,然後用 os.execve() 執行已核准的 binary。
def main() -> None:
args = parse_original_command(os.environ.get("SSH_ORIGINAL_COMMAND", ""))
if not allowed(args):
raise SystemExit(126)
executable = executable_path(args[0])
if executable is None:
raise SystemExit(126)
os.chdir("/var/empty")
os.execve(executable, [executable, *args[1:]], EXECUTION_ENVIRONMENT)
這裡有一個容易忽略的前提:wrapper、/var/empty 和 observer 用的 kubeconfig 都必須由 root 擁有,SSH 使用者不能寫入。否則固定路徑只是假象。另外,本機工具在真正送診斷前會先向遠端 wrapper 確認 policy version;版本不對就直接停止,不假設兩端規則相同。
這把 key 的目的也不是降低既有管理者帳號的權限。若那個帳號本來能操作 Docker 或叢集,它仍是受信任的 operator。限制的是「AI 使用的這把 key」:它只能做這些已審核的讀取動作,不能拿一般管理 key 取代。
Log 很吵,本機模型的 context 更小
log 和 event 不是可信輸入。它們可能有 token、內部 URL、堆疊資訊,甚至是故意放進去干擾 agent 的文字。工具會限制輸出大小、盡量遮罩敏感字串、移除控制字元,並把回傳結果標記為 untrusted evidence。這不是保證 log 安全,而是避免模型把 log 內的命令或連結當成下一步指示。
本機 LLM 還有另一個很實際的問題:tool schema 和長輸出都很吃 context。這個 plugin 原本有 56 個偏細或重複的工具,後來整併為 19 個。full profile 保留完整 schema;Codex 和 Claude Code 預設用 balanced,保留所有安全工具但把模型看到的結果限制在 4 KB;Qwen Code 和 LM Studio 則用 compact,只開 health、Pod、rollout、events、logs、Argo CD 與 Docker 等八個第一輪排查工具,回傳約 2 KB 的摘要 JSON。
這不是單純把文字截短。Kubernetes inventory 預設只給摘要和有限表格;一般診斷最多 8 KB,明確的 log 工具最多 12 KB;profile 再把送進模型的資料縮小一次。對較小的模型來說,先拿 health summary,再針對問題的 workload 看 rollout、events 或 logs,通常比一開始丟整個叢集狀態更快也更可靠。
我現在的使用方式是先讓 AI 看 GitOps repository,知道期望狀態;再看 live cluster 的 health summary;真的看到問題才往下查一個 workload 或 service。它可以整理證據、提出可能原因,甚至建議該改哪個 manifest,但不負責直接修復。最後的變更仍留在 Git diff、review 和 Argo CD 裡。
這個工具沒有讓 AI 變成 Kubernetes 管理員。它只是把平常人工會做的第一輪觀察,變成一組範圍清楚、可以重複檢查的讀取操作。對我來說,這已經足夠有用。