ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于 Claude Agent SDK 的 Kubernetes 自托管部署实战:pod-per-session 架构与网络级 egress 隔离

基于 Claude Agent SDK 的 Kubernetes 自托管部署实战:pod-per-session 架构与网络级 egress 隔离 基于 Claude Agent SDK 的 Kubernetes 自托管部署实战pod-per-session 架构与网络级 egress 隔离【免费下载链接】claude-cookbooksA collection of notebooks/recipes showcasing some fun and effective ways of using Claude.项目地址: https://gitcode.com/GitHub_Trending/an/claude-cookbooks本指南是 Agent SDK hosting cookbook 的 Tier 3 章节配套文档解析与源码级详解面向需要在自有 Kubernetes 集群上以“每会话一个 Pod”方式托管 Claude Agent 的团队。读完本文你将掌握网关如何按需创建/回收 agent Pod、如何借助 NetworkPolicy Egress Proxy 把 agent 的网络出口严格锁死到 Anthropic API、standby 预热池如何消除冷启动延迟以及如何在 kind 上本地验证整套机制后平滑迁移到 EKS / AKS / GKE 等生产集群。本 cookbook 将同一份 research agent 镜像通过三种方式托管Tier 1本地 Docker、Tier 2Modal Sandbox与Tier 3Kubernetes。三种方式的agent 镜像、HTTP 接口契约完全一致改变的只是容器外的编排与网络机制见 hosting 目录总览。在你决定自建之前如果只是想托管 agent 而不想运维基础设施应优先使用 Anthropic 的托管方案参见 Hosting 总览。本指南面向需要把 agent 放到自己 Kubernetes 集群上的团队——例如受监管环境、已有平台、需要自定义网络的场景。一、总体架构为什么是 pod-per-sessionTier 3 的核心思想每个用户会话拥有一个完全隔离的 Pod同时通过网络层控制保证 agent Pod 只能访问 Anthropic API。整张拓扑图如下摘自 kubernetes/README.md┌──────────────────────────────────────────────────┐ │ Kubernetes │ │ │ curl / SDK ──────► Gateway (FastAPI) │ │ ├─ creates/deletes agent pods via K8s API │ │ ├─ routes /sessions/{id}/messages to right pod │ │ └─ session → pod mapping stored in Redis │ │ │ ┌──────┴──────┐ │ │ │ │ Agent Pod Agent Pod ──► Egress Proxy ──► api.anthropic.com │ (session A) (session B) ▲ │ │ │ │ │ │ NetworkPolicy: pods can ONLY reach egress-proxy │ │ │ Redis (session → pod-IP mapping) │ │ │ └──────────────────────────────────────────────────┘关键事实agent 镜像就是 Tier 1 用 hosting/Dockerfile 构建出的同一个镜像。同一镜像、不同的编排机制——不再是单个容器或 Modal Sandbox而是由网关给每个会话分配独立 Pod并由集群强制该 Pod 的可达范围。这里必须理解托管层定义并严格遵守的HTTP 接口契约见 hosting/README.mdGET /health→200 {status: ok}作为存活探针POST /sessions/{session_id}/messages请求体为{prompt: 用户消息}返回200 text/event-stream事件流中包含event: messagedata 是序列化的 SDK 消息SystemMessage / AssistantMessage / ResultMessage、event: done本轮结束、event: errorsession_id必须匹配^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$非法则返回 400容器端口 8000必需环境变量ANTHROPIC_API_KEY该 server 默认无认证必须放在网关后面禁止直接暴露到公网。正因为三层共用同一契约网关代码注释里才写明针对 Docker / Modal 层写好的客户端代码在 Kubernetes 层无需修改即可复用只是 base URL 变了。二、每个组件为什么存在Gateway网关——按需调度 Pod 的唯一入口每个用户会话都有自己的 agent Pod那么必须有东西按需创建 Pod、把流量路由到正确 Pod、在会话空闲时回收——这就是网关。它通过 Kubernetes API 管理 Pod 生命周期并用 Redis 记住哪个 session 映射到哪个 Pod IP。从源码看gateway/main.py 的职责非常收敛用 Redis hashsession:{id}保存session_id → pod_ip映射与归属租户ownershipPOST /sessions/{id}/messages先_ensure_session_pod()拿到 Pod IP再由relay_sse()把请求转发给 agent Pod 并把 SSE 字节流原样回传proxy.py后台_reap_idle_loop()每 60 秒扫描一次sessions:active集合把超过IDLE_TIMEOUT_S无活动的会话 Pod 删除。网关自身是无状态的路由元数据全在 Redis因此可以在负载均衡器后面横向扩展多个副本。Pod 管理相关的函数create_agent_pod、delete_agent_pod、get_pool_status、initialize_standby_pool全部集中在 gateway/k8s.pymain.py只做路由与回收。Egress Proxy NetworkPolicy —— 网络层出口白名单Agent 会执行模型决定运行的任意代码。这一对机制保证 agent Pod 能访问api.anthropic.com并且只能访问它NetworkPolicy 拦截所有出站流量只放行到 egress proxy443 端口与 DNS53 端口Egress proxy 终结来自 agent 的 TLS再重新加密转发到 Anthropic API任何访问互联网、其他服务或其他命名空间的尝试都会在网络层被丢弃。具体到 network-policy.yaml它选中所有role: agent标签的 Pod同时声明Ingress与Egress两类策略入站只允许带app: gateway标签的 Pod 连接 agent Pod其余一律丢弃——集群里没有任何其他服务能直接触达 agent出站仅允许连到app: egress-proxyTCP 443与任意目标做 DNS 查询UDP/TCP 53其余全部丢弃。在 nginx.conf 这一侧nginx 把anthropic_apiupstream 指向api.anthropic.com:443开启proxy_ssl_verify on校验上游证书、proxy_ssl_server_name onproxy_ssl_name api.anthropic.com设置 SNI并关闭proxy_buffering以支持流式响应长连接超时给到proxy_read_timeout 300s。nginx 配置里还专门声明了两层防护互补的模型NetworkPolicy 保证 agent 只能到达 proxyproxy 则保证只转发到 Anthropic API——两层共同构成严格的网络 allowlist。值得注意的工程取舍写在该配置头注释里api.anthropic.com只在 nginx 启动时通过集群 DNS 解析一次不依赖外部解析器——这意味着这个隔离组件对外部零依赖、零元数据泄露代价是若 Anthropic API 的 IP 长期漂移需要重启 egress-proxy Deployment 重新解析。Redis —— 路由元数据的唯一事实源网关需要记住哪个 Pod 在服务哪个会话。请求到达时先按 session ID 查 Redis 找到 Pod IP再把流量路由过去。Redis 开启--appendonly yes持久化到 PVC1Gi见 redis.yaml因此映射能跨网关重启存活。redis.yaml底部还附带一条redis-ingress-policyNetworkPolicy只允许app: gateway访问 Redis 6379。理由写得很清楚——agent Pod 虽已被它们自己的 egress 策略挡住但若命名空间里出现任何其他Pod没有这条规则就可能改写session → pod-IP路由表。而 agent Pod 的 k8s.py 里也做了对称加固automount_service_account_tokenFalse连 K8s API 凭据都不挂进运行模型驱动代码的容器里。Standby Pool —— 预热池消除 10–30 秒冷启动Pod 启动需要 10–30 秒拉镜像 容器启动。网关预热一个可配置数量的 standby Pod新会话到来时可以立即认领而不用等待Pod 被认领后池子在后台自动补足。k8s.py 完整实现了这套生命周期管理核心函数包括_build_pod_manifest()构造 agent Pod 的 manifest环境变量里把 API key 从anthropic-api-keySecret 通过secretKeyRef注入、把ANTHROPIC_BASE_URL指向https://egress-proxy、通过NODE_EXTRA_CA_CERTS/certs/ca.crt让 Node.js 信任自签名 CA、设置CLAUDE_CONFIG_DIR/data并挂载readiness_probeHTTP GET/healthinitial_delay_seconds2, period_seconds2, failure_threshold15以确保认领前 uvicorn 真的在监听_claim_standby_pod()通过原子 PATCH Pod 标签把pool-status: standby改成active并打上session-id——如果两个网关实例竞争认领同一个 Pod只有一个 PATCH 会成功失败方自动尝试下一个全程无需外部锁_replenish_pool()认领或删除后后台补池计数时把Pending仍在拉镜像的 Pod 也计入避免慢速拉镜像导致过度预置循环上限为STANDBY_POOL_SIZE * 2防止镜像持续拉取失败时无限空转create_agent_pod()的策略是先试认领预热 Pod即时返回没有可认领对象再按agent-session-{session_id}创建按需 Pod 并轮询等待其 Readydelete_agent_pod()先按session-id标签查删再回退到确定性名字全部幂等处理 404。值得注意的是agent Pod 的容器以args[serve]启动k8s.py配合restart_policyNever与termination_grace_period_seconds5——agent Pod 是按会话存在的临时实体删了就没了无需优雅排空。三、前置条件工具用途kind在 Docker 里跑一个本地 Kubernetes 集群kubectl应用 manifest、检查集群状态docker构建容器镜像openssl生成 egress proxy 的 TLS 证书ANTHROPIC_API_KEY以环境变量形式提供此外 kind-quickstart.sh 还会检查jq是否已安装。四、Quickstart在 kind 上跑通本地端到端Tier 3 的快速启动脚本位于 kind-quickstart.sh。kindKubernetes in Docker会在你笔记本的 Docker 容器内启动一个真实的 Kubernetes API server无需云账号脚本应用的 manifests 与生产集群是同一批只是镜像仓库不同。cd hosting/kubernetes export ANTHROPIC_API_KEYsk-ant-... ./kind-quickstart.sh脚本会完成以下工作创建 kind 集群——注意它显式disableDefaultCNI: true并安装Calico因为 kind 的默认 CNIkindnet不执行 NetworkPolicy详见下文验证出口封锁一节的说明创建后依次等待calico-nodeDaemonSet、calico-kube-controllers及节点就绪构建并装载三个镜像到 kindagent复用 Tier 1 的 hosting/Dockerfile构建上下文是上一级claude_agent_sdk/因为镜像需要research_agent/与utils/、gateway、egress-proxy再kind load docker-image运行 generate-certs.sh生成 egress proxy 的自签名证书应用 namespace、三个 Secret 与一个 ConfigMapanthropic-api-key、gateway-tenants、egress-proxy-tls以及agent-config内含AGENT_IMAGElocal/agent:latest与STANDBY_POOL_SIZE2其中 tenant Secret 用openssl rand -hex 16生成两个演示租户 token映射为alice与bob应用全部 manifests用sed s|REGISTRY_URL|local|g做占位符替换等待redis、egress-proxy、gateway三个 Deployment 变为 available把 gateway 端口转发到localhost:8080并打印两个演示租户的 bearer token。脚本还会在开头做一次API key 预检直接请求https://api.anthropic.com/v1/models验证 key 有效避免一个失效的 key 等到整套集群起来、第一次 curl 时才暴露成难懂的 Invalid API key。脚本结束时输出的两个 token 需要导出后使用export ALICE_TOKEN... # 由 kind-quickstart.sh 打印 export BOB_TOKEN...五、与 agent 对话请求路径与形状和 Tier 1/2 一致——只是 base URL 变了且网关现在要求携带标识调用方租户的 bearer tokencurl -N -X POST http://localhost:8080/sessions/demo/messages \ -H Authorization: Bearer $ALICE_TOKEN \ -H Content-Type: application/json \ -d {prompt: What tools do you have?}会话语义某个session_id上的第一个请求会认领一个 standby Pod池子为空时则新建一个后续同一session_id的请求会路由到同一 Pod因此 agent 能读到连续的对话历史。租户隔离语义会话现在归属alice——网关在 Redis 里记录了创建者租户并在每一次后续请求上复查归属。同一个调用换成$BOB_TOKEN会返回403 {detail:session belongs to another tenant}不带 token 则返回401。从 main.py 源码看这层检查的实现细节很完整租户映射是gateway-tenantsSecret 里一段静态的token:tenant,token:tenant字符串启动时解析进_TOKEN_TO_TENANT空 token 的配对会被丢弃防止空 Authorization 头成为合法凭据认证函数authenticate()遍历映射并用secrets.compare_digest逐一比对避免把查找过程变成时序侧信道GATEWAY_TENANTS未设置时则放行所有请求并把调用方标记为单租户anonymous仅限本地调试_ensure_session_pod()用 Redis 的SET NX加一把带PROVISION_TIMEOUT_S180 秒TTL 的 per-session 锁来防并发预置若客户端重试与正在预置的请求撞车可能为同一会话认领第二个 Pod 并泄漏到空闲回收器运行锁竞争失败方会每秒轮询等待赢家的映射出现且之后仍然走一遍归属检查——两个租户同时抢建同一session_id也不可能共享一个 Pod。观察整台机器运转Pod 生命周期可视化kubectl -n claude-agent get pods -w # 你会看到 agent-standby-* Pod 出现随后当你 curl 时其中一个翻转为 active要结束一个会话请走网关同样仅限拥有者与所有会话操作一致这样 Redis 映射才会被清理curl -X DELETE http://localhost:8080/sessions/demo \ -H Authorization: Bearer $ALICE_TOKENkubectl delete pod也能删掉 Pod但会在 Redis 里留下过期的session → pod-IP条目直到该会话的下一个请求 502 为止。源码里 main.py 的delete_session处理得非常细致删除不存在的会话返回 200 no-op 且完全不碰集群——因为 Pod 可能在 Redis 记录写入前就已存在误删会误杀其他租户仍在预置中的 Pod。值得一提的是 main.py 对Pod 已死但映射仍在的恢复逻辑当转发收到 502映射的 Pod 被驱逐 / OOM 杀死 / 节点重启网关会删除死 Pod 对象、只删掉 Redis hash 里的pod_ip字段保留其余字段使会话不会变成无主状态、别的租户便无法趁恢复期抢占该 ID然后重新预置一个 Pod 再转发一次。六、验证出口封锁提示注入的 agent 也出不去Agent 运行的是模型决定要运行的代码。egress proxy NetworkPolicy 意味着即便 agent 被提示注入仍然无法访问任意主机。我们来证明这一点kind-quickstart.sh之所以安装 Calico是因为 kind 的默认 CNIkindnet不执行 NetworkPolicy。在 GKE / EKS / AKS 或任何 Calico / Cilium 集群上网络策略默认即被强制执行本节可以原样运行。AGENT_POD$(kubectl -n claude-agent get pods -l roleagent \ -o jsonpath{.items[0].metadata.name}) # 这一步应当失败——Calico 会丢弃除 egress-proxy 外的一切路由。 # agent 镜像很精简、没有 curl所以我们用 Python 的 socket。 kubectl -n claude-agent exec $AGENT_POD -- python3 -c \ import socket; socket.setdefaulttimeout(5); socket.create_connection((example.com,443)); print(REACHED — policy NOT enforcing)预期结果是OSError: [Errno 101] Network is unreachable或超时并伴随非零退出码。而正向对照——egress-proxy 路径是通的——已被上文那次返回了模型输出的 curl 证明过了。关于 DNS 残余通道由于 53 端口保持对所有解析器开放保证 NodeLocal DNS 缓存可用DNS 隧道仍是理论上的数据外泄残留通道。network-policy.yaml 的注释给出了收紧建议如果你的集群直接与 kube-dns/CoreDNS Pod 通信无节点级 DNS 缓存可把 DNS 规则收窄为namespaceSelector: kubernetes.io/metadata.name: kube-system或在解析器层强制 DNS 策略CoreDNS ACL、Cilium DNS policies。七、Standby 预热池的运行时观察agent-configConfigMap 里的STANDBY_POOL_SIZE控制网关保持多少个热 Pod。查看当前池状态任意合法租户 tokencurl http://localhost:8080/api/pool -H Authorization: Bearer $ALICE_TOKEN对应的实现是 k8s.py 的get_pool_status()返回{ target_size: N, ready_count: n, standby_pods: [...] }其中ready_count只统计真正可认领的 Pod——要求Running、有 Pod IP、无deletion_timestamp、且全部容器ready_pod_is_ready()。main.py的/api/pool端点适合接进监控。八、持久化会话历史存在哪里丢了怎么办hosting/server.py会把会话记录及其 caller-ID → SDK-ID 映射持久化到CLAUDE_CONFIG_DIR/data。在 Tier 3 里/data是Pod 的临时文件系统默认以 emptyDir 语义挂在 Pod 本地因此Pod 存活期间处于 idle-timeout 窗口内后续消息能精确续上对话与 Tier 1/2 行为一致Pod 被回收后/data随之消失。该session_id的下一条消息会拿到一个无历史的全新 Pod。对 cookbook 演示来说这完全够用——会话的寿命超过 curl但不需要超过集群。生产环境则需要能在 Pod 回收后存活的持久化存储两种方案挂载 PersistentVolumeClaim 到/data并让网关在会话回归时重新挂载同一 PVC。server.py可原样工作但每个会话会与某个可用区的卷耦合。把/data镜像到外部存储借助 Agent SDK 的SessionStore本地磁盘写入仍然先行发生store 只是镜像mirror_error非致命。这是 notebook 里Making it production-ready一节推荐的做法——需要在server.py里加一个小钩子cookbook 目前还没有实现。九、部署到你自己的集群kind验证的是拓扑manifests 本身是云无关的。要在 EKS、AKS、GKE、OpenShift 或裸机上运行只需替换镜像仓库与前端的入口REGyour.registry.example.com/claude-agent # ECR、ACR、GHCR、Artifact Registry…… # 1. 构建并推送三个镜像 docker build -t $REG/agent:latest -f ../Dockerfile .. docker build -t $REG/gateway:latest ./gateway docker build -t $REG/egress-proxy:latest ./egress-proxy docker push $REG/agent:latest $REG/gateway:latest $REG/egress-proxy:latest # 2. 为 egress proxy 生成 TLS 证书 ./generate-certs.sh # 3. namespace secrets config kubectl apply -f manifests/namespace.yaml kubectl -n claude-agent create secret generic anthropic-api-key \ --from-literalANTHROPIC_API_KEY$ANTHROPIC_API_KEY kubectl -n claude-agent create secret generic gateway-tenants \ --from-literalGATEWAY_TENANTS$(openssl rand -hex 16):tenant-a,$(openssl rand -hex 16):tenant-b kubectl -n claude-agent create secret generic egress-proxy-tls \ --from-fileca.crtcerts/ca.crt \ --from-fileproxy.crtcerts/proxy.crt \ --from-fileproxy.keycerts/proxy.key kubectl -n claude-agent create configmap agent-config \ --from-literalAGENT_IMAGE$REG/agent:latest \ --from-literalSTANDBY_POOL_SIZE2 # 4. 替换仓库占位符后应用 manifests for f in manifests/*.yaml; do sed s|REGISTRY_URL|$REG|g $f | kubectl apply -f - done若日后修改了$REG必须同时重建agent-configConfigMap——网关派生 agent Pod 时读取的是其中的AGENT_IMAGE只对 manifests 重跑sed并不会重新指向新镜像。关于generate-certs.sh需要说明的是它产出的四件套certs/ca.keyCA 私钥需保密、certs/ca.crtCA 证书分发给 agent Pod 使其信任代理、certs/proxy.key与certs/proxy.crt由自建 CA 签名的代理证书挂载进代理 Pod。为什么需要自签名egress proxy 是内部服务而非公网站点无法从公共 CA 取证书因此自行创建 CA 并为egress-proxy、egress-proxy.claude-agent.svc.cluster.local、localhost三个 SAN 签发证书agent Pod 再通过NODE_EXTRA_CA_CERTS显式信任该 CAnginx.conf 与 generate-certs.sh 中均有完整解释。随后通过集群惯用的方式暴露gatewayService——云 LoadBalancer、Ingress controller 或 service mesh gateway 均可。有三个随环境而变的要点Registry 认证——节点需要$REG的拉取凭据imagePullSecrets、IRSA / Workload Identity或公共镜像仓库NetworkPolicy 执行能力——出口封锁只在你的 CNI 执行NetworkPolicy时才生效Cilium、Calico、GKE Dataplane V2、启用 VPC CNI policy add-on 的 EKS。若是忽略策略的 CNIagent Pod 将能访问互联网Gateway 前面的 TLS 与认证——静态GATEWAY_TENANTStoken 映射只是真实凭据的替身。对外暴露前务必在网关前放置你的 IdP / API gateway。RBAC 的划分在 gateway.yaml 中也很值得学习网关以gateway-sa身份运行通过gateway-pod-managerRoleverbs 仅限 pods 的create/get/list/patch/delete限定在claude-agent命名空间获得 K8s API 权限——恰好是k8s.py会发出的那些调用而 agent Pod 使用没有任何 Role的agent-sa配合automount_service_account_tokenFalse彻底不需要 K8s API 访问。十、这套方案没给你什么诚实的边界真正的身份提供方。网关确实强制了每租户的会话归属——GATEWAY_TENANTS里每个 bearer token 映射一个租户创建租户拥有会话其他租户得到 403——但 token 本身是静态映射没有签发、轮换、吊销或 per-tenant RBAC。把authenticate()换成你的 IdP 即可归属检查逻辑无需改变参见 main.py 的完整注释。持久的会话存储见上文持久化一节。网关自动扩缩或多区域路由。DNS 级出口控制——53 端口保持对任意解析器开放见验证出口封锁一节。加固过的支撑服务——gateway、Redis 与 nginx 都以其官方镜像的默认root用户运行。加固预算被集中投给了运行模型驱动代码的 agent Pod其余服务在上生产前应按你所在组织的基线锁定。超出OTEL_EXPORTER_OTLP_ENDPOINT免费赠送范围之外的可观测性。十一、清理与目录布局本地验证完毕后运行脚本拆除集群与证书./teardown.sh # kind delete cluster remove certs/完整布局如下文件说明摘自 kubernetes/README.mdkubernetes/ ├── README.md ├── kind-quickstart.sh # 在 kind 上跑通本地端到端 ├── teardown.sh ├── generate-certs.sh # 为 egress-proxy 生成自签名 CA 代理证书 ├── gateway/ │ ├── main.py # FastAPI路由 回收含租户认证与按需建 Pod │ ├── k8s.py # Pod 生命周期 standby 预热池 │ ├── proxy.py # SSE 透明中继不做解析、逐字节转发 │ ├── requirements.txt │ └── Dockerfile ├── egress-proxy/ │ ├── nginx.conf # 出口白名单只代理 api.anthropic.com │ └── Dockerfile └── manifests/ ├── namespace.yaml # 所有资源隔离在 claude-agent 命名空间 ├── redis.yaml # Redis含 1Gi PVC 与只许网关访问的入站策略 ├── egress-proxy.yaml ├── gateway.yaml # SA RBAC Deployment Service含三探针 └── network-policy.yaml # 入站只许网关、出站只许 proxy/DNS 的双向封锁如果还想继续深入建议配合阅读 07_Hosting_the_agent.ipynb托管决策的完整叙述什么时候托管方案更合适、什么时候你才真正需要 Tier 3、hosting/README.md三层共用的接口契约与镜像构建方式以及 gateway/main.py、gateway/k8s.py、egress-proxy/nginx.conf 这些第一手实现——架构图上每一条线都能在这几个文件里找到对应的代码。【免费下载链接】claude-cookbooksA collection of notebooks/recipes showcasing some fun and effective ways of using Claude.项目地址: https://gitcode.com/GitHub_Trending/an/claude-cookbooks创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表