Python Kubernetes客户端实战:告别kubectl脚本,实现自动化运维
这年头搞开发不会点 Kubernetes 都显得不合群。但真正落到日常开发、运维、自动化交付时你会发现一个尴尬的现实敲 kubectl 命令一时爽脚本一多就开始痛。尤其是遇到批量查 Pod 状态跨集群更新镜像定期清理 Evicted Pod这类高频操作用 shell 堆 kubectl 简直就是行为艺术——每个集群要拼 KUBECONFIG输出是给你人看的表格不是给你程序用的 JSON一上规模就崩。我的解决思路很直接直接用 Python Kubernetes 客户端官方kubernetes这个 PyPI 包把 API 调用串起来做成模块化的脚本和工具。这篇文章就是一份实操笔记覆盖从 kubeconfig 加载、API Group 理解、Pod/Deployment/Service 的增删改查到 Watch 监听、动态客户端、CRD 操作、性能优化和常见报错排查。目标读者是对 Python 有基础、用过 kubectl、想摆脱敲命令模式的工程师。看完你至少能动手写出第一个创建 Deployment 并确认 ready的完整脚本而不是只会打印一堆不认识的字段。1. 整体设计为什么 Python 客户端比 kubectl 脚本更值得投入先回答一个很多人纠结的问题既然 kubectl 背后调的就是 API Server那kubectl get和Python 客户端调用到底差在哪我直接说结论kubectl 是给你人用的Python 客户端是给你程序用的。程序需要的是稳定的数据结构、可重试的调用、可编程的流式处理而 kubectl 的输出天然是给人做排除问题用的。1.1 直接写 REST 请求的痛和 SDK 解决的是什么有人会说那我不就不用客户端库了反正 K8s 有完整的 REST API我用 requests 包直接 POST /apis/apps/v1/namespaces/default/deployments 不就行了我这么试过结果是一地鸡毛。直接写 REST 请求意味着你要自己处理JSON 序列化和反序列化的字段映射。K8s API 返回的字段巨多status.conditions是个嵌套数组手写解析容易崩溃。认证方式。token、client certificate、kubeconfig 里的加密 key你要自己解析证书和私钥做 TLS 双向认证。手写起来能把人写哭。分页、Watch 长连接、补丁类型、重试机制。这些都是看似简单细节一堆的活。官方客户端库基于 K8s 的 OpenAPI 规范生成字段、类型、接口路径都已经映射好你要做的只是选择正确的方法。比如list_namespaced_pod、create_namespaced_deployment方法名就是 HTTP 动词加上资源类型很好猜。1.2 一张地图看懂 API Group 与 GVK很多刚开始用客户端库的人第一个报错不是 401而是 404然后一脸懵。原因是不理解 K8s 的 API 组织方式API Group Version Kind也就是常说的 GVK。K8s 的 API 不是一平层的。我习惯用一个类比去理解API Group 是公司的部门Version 是部门的规章制度版本Kind 是具体岗位。找人办事前你得先知道这个岗位在哪个部门。最核心的几个API Group路径前缀常见 Kind用途core空 group/api/v1Pod、Service、ConfigMap、Secret、Namespace、Node、PV/PVC基础设施层资源最底层的东西apps/apis/apps/v1Deployment、StatefulSet、DaemonSet、ReplicaSet应用编排层日常部署关注最多batch/apis/batch/v1Job、CronJob一次性任务、定时任务networking.k8s.io/apis/networking.k8s.io/v1Ingress、NetworkPolicy网络入口与策略rbac.authorization.k8s.io/apis/rbac.authorization.k8s.io/v1Role、ClusterRole、RoleBinding权限控制在 Python 客户端里Group 决定了你用哪个 API 类的实例。Pod属于 core group用CoreV1ApiDeployment属于 apps group用AppsV1ApiJob属于 batch group用BatchV1Api。你要是拿CoreV1Api去建 DeploymentAPI Server 会直接给你 404因为/api/v1路径下压根没有这个资源。实操建议不确定某个资源属于哪个 group先跑一条命令在集群里查一下kubectl api-resources | grep deployment。这个命令输出的 APIVERSION 列就是客户端里应该填的 apiVersion。2. 环境初始化与两个不一样的 API 对象搞懂了资源地图下一步是让客户端能连上集群。这一步卡住的概率极高因为 K8s 的连接方式和传统数据库连接完全不是一个路子。2.1 三种初始化方式选错会被卡在最前面Python 客户端的认证信息来源是 kubeconfig 文件。默认情况下load_kube_config()会按顺序查找$KUBECONFIG环境变量指向的文件以及~/.kube/config。本地开发、日常测试基本就是这种方式它会把你当前 kubectl 使用的 context 直接读出来token、client-certificate 这些都用不上你操心。但是注意一旦脚本跑在 Pod 内部情况完全变了。容器里没有~/.kube/config这时要改用load_incluster_config()。K8s 会自动把 ServiceAccount 的 token 挂载到/var/run/secrets/kubernetes.io/serviceaccount/目录API Server 的地址和端口通过KUBERNETES_SERVICE_HOST、KUBERNETES_SERVICE_PORT环境变量注入。这个函数读取的就是这些信息。还需要第三种情况你在本地调试但没有现成的 kubeconfig只想连一下测试集群的 6443 端口。可以用原生Configuration对象直连把 token 和 host 写死from kubernetes import client configuration client.Configuration() configuration.host https://127.0.0.1:6443 configuration.api_key[authorization] Bearer token configuration.verify_ssl False api_client client.ApiClient(configuration) v1 client.CoreV1Api(api_client)verify_ssl False只在本地调试临时集群时才这么干生产环境必须改成 True 并挂载 CA 证书。这个直连方式还有个用途是快速验证集群 API Server 通不通如果直连能通但客户端连不上问题多半出在 kubeconfig 解析上。多集群场景我再补一句load_kube_config()支持context参数比如config.load_kube_config(contextcluster-b)可以在一个脚本里管理多个集群。但注意每个调用会改变全局的默认配置多线程并发时涉及读同一个Configuration会互相干扰。我后来干脆给每个集群建独立的ApiClient实例彻底避免共享状态问题。2.2 CoreV1Api 与 AppsV1Api先搞清楚两者的分工新手最容易犯的错拿到客户端库以后看到什么都往CoreV1Api身上挂。它确实是最常用的Pod、Service、ConfigMap、Secret 都在里面但 Deployment 不在。我举个具体的串联场景你就明白了。你要发布一个应用流程是用AppsV1Api创建 Deployment用CoreV1Api查询 Pod 状态、读取日志用CoreV1Api创建 Service 暴露访问入口。这两个 API 对象天生就是要配合使用的不存在二选一的问题。定位方法很有规律涉及应用编排状态的资源比如副本数、滚动更新策略、持久化存储状态基本在AppsV1Api。涉及工作负载运行实体的资源比如具体某个 Pod 在哪个节点、它的日志是什么、它的 IP 是什么基本在CoreV1Api。打个比方Deployment是你的需求文档Pod是真正干活的员工。AppsV1Api管理需求文档和排班表CoreV1Api管员工本人的考勤和开工资。两套体系职责不同但都要用。2.3 推荐的写 YAML 方式从文件到 API 对象的完整链路创建资源的写法有两种一种是手工构造V1Deployment、V1PodTemplateSpec这样的嵌套对象另一种是直接读 YAML 转 dict。我强烈推荐第二种原因很简单你的团队大概率已经有了 YAML 格式的部署清单可能是 Helm 模板渲染后的产物也可能就是 Kubernetes 集群里正在跑的 manifest。直接用 Python 去拼接对象不仅代码啰嗦还容易和现有运维体系脱节。正确姿势是用yaml.safe_load加载文件然后直接作为body参数传给 API 方法import yaml from kubernetes import client, config config.load_kube_config() with open(deployment.yaml, r, encodingutf-8) as f: dep yaml.safe_load(f) apps_v1 client.AppsV1Api() resp apps_v1.create_namespaced_deployment( namespacedefault, bodydep )这里的body接受的是 dictPython 客户端内部会帮你映射成 API 请求体。为什么推荐这样因为 K8s 的 API 本来就是声明式的YAML 就是最贴近声明式理念的格式。你把文件里replicas: 3改成replicas: 5代码一行不用动比硬编码在 Python 里好维护得多。踩坑提醒YAML 转 dict 后字段名必须和 API 规范完全一致。很多人把apiVersion写成api_version或者把metadata里的namespace放在 YAML 最外层——这些都会导致创建失败报错信息有时候还很不直观。遇到字段名报错第一反应应该是去查这条资源的 OpenAPI 定义而不是瞎猜。3. 核心 API 串联实战创建一个能访问的 Deployment这一节是整篇文章的重点。我把从创建 Deployment到通过 Service 访问的完整链路拆开讲每一步都会说清楚 API 参数为什么这么写、常见坑在哪。3.1 创建 Deployment 的完整代码与参数拆解先来一个标准到不能再标准的 Deployment 创建from kubernetes import client, config config.load_kube_config() apps_v1 client.AppsV1Api() deployment { apiVersion: apps/v1, kind: Deployment, metadata: { name: nginx-deploy, namespace: default, labels: {app: nginx} }, spec: { replicas: 3, selector: { matchLabels: {app: nginx} }, template: { metadata: { labels: {app: nginx} }, spec: { containers: [ { name: nginx, image: nginx:1.25, ports: [{containerPort: 80}] } ] } } } } apps_v1.create_namespaced_deployment( namespacedefault, bodydeployment )这里有个必须强调的点spec.selector.matchLabels和spec.template.metadata.labels必须互相匹配。selector是 Deployment 控制器筛选自己管理的 Pod 的依据它是不可变的。创建之后如果改了 selector 里的 labelAPI Server 会拒绝更新报Invalid value: ... field is immutable。所以创建前就要想好 label 规划不要指望后面能随便改。为什么apiVersion是apps/v1而不是extensions/v1beta1extensions/v1beta1是 K8s 1.7 时代的老版本早就废弃了。如果网上找到的示例还写着这种旧 apiVersion千万别直接抄。这也说明理解 GVK 不只是为了找对路径还是为了避开过期 API 的地雷。3.2 等待 Pod Ready别用 sleep用状态判断创建完 Deployment 之后很多人直接time.sleep(10)然后去查 Pod。这在本地环境可能没问题但在资源紧张或者镜像拉取慢的环境里10 秒根本不够Pod 可能还在 Pending。正确方法是做状态轮询。我的做法是写一个wait_for_deployment_ready辅助函数轮询 Deployment 的状态字段import time from kubernetes import client, config config.load_kube_config() apps_v1 client.AppsV1Api() def wait_for_deployment_ready(name, namespace, desired_replicas3, timeout300): start time.time() while time.time() - start timeout: dep apps_v1.read_namespaced_deployment(namename, namespacenamespace) status dep.status if status and status.ready_replicas desired_replicas: print(fDeployment {name} is ready) return True print(fWaiting: ready_replicas {status.ready_replicas if status else 0}) time.sleep(5) raise TimeoutError(fDeployment {name} not ready in {timeout}s) wait_for_deployment_ready(nginx-deploy, default)为什么要读ready_replicas而不是看 Pod phase因为 Deployment 是声明式控制器它会持续调整副本数。ready_replicas表示真正可对外服务的 Pod 数量这个字段达到预期值说明整条链路都通了——Pod 创建成功、调度成功、容器启动成功、就绪探针通过。另外提醒一点read_namespaced_deployment的这种轮询方式请求频率要控制住。我一般 5 秒一次最多 300 秒超时避免把 API Server 打出不必要的负载。3.3 暴露 Service 并验证 endpointsDeployment 创建完成Pod 也起来了但这时候它们只有集群内部 IP而且是会变化的。要做到稳定访问必须创建 Service。Service 会把一组 Pod 抽象成一个稳定的虚拟 IPClusterIP和 DNS 名称。核心代码from kubernetes import client, config config.load_kube_config() v1 client.CoreV1Api() service { apiVersion: v1, kind: Service, metadata: { name: nginx-svc, namespace: default }, spec: { selector: {app: nginx}, ports: [ { port: 80, targetPort: 80 } ] } } v1.create_namespaced_service(namespacedefault, bodyservice)这里最容易踩的坑是 selector 和 endpoints 对不上。spec.selector必须“精准命中”Pod 上的 labels。如果 Deployment 的 Pod 模板里写的是app: nginxService 的 selector 里写app: nginxv2那这个 Service 创建后是空壳——endpoints 列表永远是空的流量进来直接断。验证方法也走 APIeps v1.read_namespaced_endpoints(namenginx-svc, namespacedefault) print(eps)如果 endpoints 里有类似10.244.0.5:80的地址说明 Service 和 Pod 已经成功关联。如果为空第一件事不是查网络而是检查 labels 是否匹配。targetPort这个字段也值得说。它可以写成数字80也可以写成容器端口名称比如容器里定义了name: http那targetPort: http也行。数字直观名称解耦建议团队里约定一致。3.4 镜像更新、滚动发布与回滚发布之后总有一天要升级镜像。修改 Deployment 镜像有两种方式replace全量替换和patch局部更新。先看 patch 方式修改副本数这种单字段操作最合适apps_v1.patch_namespaced_deployment( namenginx-deploy, namespacedefault, body{ spec: { replicas: 5 } } )改镜像也差不多apps_v1.patch_namespaced_deployment( namenginx-deploy, namespacedefault, body{ spec: { template: { spec: { containers: [ { name: nginx, image: nginx:1.26 } ] } } } } )重点说说 replace 和 patch 的区别。replace是你给什么我就替换成什么等于你先read_namespaced_deployment拿到完整对象改字段后再全量提交。这个操作要求你提交的对象里的resourceVersion和集群当前版本一致否则 API Server 返回 409 Conflict。所以用 replace 的正确步骤是先 read → 修改内存对象 → replace。patch 则不同它只提交变化的部分不会碰其他字段更不容易出错也更推荐日常使用。滚动发布如何判断发布成功回到状态字段status.updated_replicas已经更新到新版本的副本数status.ready_replicas就绪副本数status.available_replicas对外可用副本数当三者都等于期望副本数时滚动发布基本完成。用前面写的轮询函数换成检查这三个字段的逻辑就行。回滚怎么办K8s 自带的kubectl rollout undo底层是把 Deployment 的 pod template 恢复到历史版本。Python 客户端没有直接封装这个命令但思路很简单把spec.template.spec.containers[0].image改回上一个版本号然后 replace 或 patch 回去。这和手动kubectl set image是一样的效果。所以我建议在生产脚本里维护一个当前版本和上一个版本的记录表而不是每次去猜历史版本号。4. Watch、日志与动态客户端处理动态变化和复杂资源脚本场景不只有创建→等待→完成这种同步流程。还有一类需求是持续的盯着资源变化、实时采集日志、操作自定义资源。这三块涉及不同的客户端能力。4.1 用 Watch 做实时事件监听原理与代码K8s API 有一个很强大的能力Watch。你可以通过打开一个长连接持续接收资源的变更事件ADDED、MODIFIED、DELETED。理解 Watch 不用太复杂类比一下kubectl 轮询是每隔 5 秒问一次现在怎么样Watch 是加了个群群里一有变化就有人通知你。前者浪费请求后者实时且高效。Python 客户端的 Watch 用起来非常清爽from kubernetes import client, config, watch config.load_kube_config() v1 client.CoreV1Api() w watch.Watch() for event in w.stream(v1.list_namespaced_pod, namespacedefault): event_type event[type] # ADDED / MODIFIED / DELETED pod event[object] print(f{event_type}: {pod.metadata.name})这段代码会一直阻塞直到你手动中断。实际使用中你要设定退出条件比如监测到特定 Pod 变化后w.stop()。这里有个很多人都没注意到的点w.stream()的底层是调用 list 接口加watchtrue参数。如果长时间运行连接可能因超时断开你需要在外面包一层重连逻辑或者利用timeout_seconds参数让 stream 周期性退出后重新连接。我自己的经验Watch 适合做事件采集和资源同步不适合做每个事件都触发一次重量级计算的傻循环。你要是拿到一个 DELETED 事件就重新 full list 一遍整个集群那还不如直接轮询。4.2 日志读取与本地调试排查问题离不开日志。读取 Pod 日志在CoreV1Api里是read_namespaced_pod_log。基本用法logs v1.read_namespaced_pod_log( namenginx-deploy-xxx, namespacedefault, containernginx, tail_lines100 ) print(logs)多容器 Pod 必须指定container参数不然在 K8s 1.10 之后的版本里部分场景会报错a container name must be specified for pod xxx。followTrue可以实现kubectl logs -f的效果实时读取日志流。但注意这本质上是一个长连接读取时会持续占用内存。我建议对大规模日志做限制先用tail_lines限定尾部行数不要一上来就把整个 Pod 的所有日志拉进内存几百 MB 的日志文件能直接让你的脚本 OOM。4.3 动态客户端操作 CRD前面讲的CoreV1Api、AppsV1Api都是静态客户端——API 方法已经固定资源类型是写死的。遇到自定义资源CRD比如你装了一个 Prometheus Operator里面有ServiceMonitor这个自定义资源静态客户端就不认识了。这时候要用DynamicClient。from kubernetes import config, client from kubernetes.dynamic import DynamicClient config.load_kube_config() dyn DynamicClient(client.api_client.ApiClient()) resources dyn.resources.get(api_versionmonitoring.coreos.com/v1, kindServiceMonitor) # 列出某个 namespace 下的所有 ServiceMonitor for item in resources.get(namespacedefault).items: print(item.metadata.name)动态客户端最大的特点是一切皆资源。你只需要提供api_version和kind客户端会自己去 Discovery 接口查询这个资源的 schema然后生成可调用的资源对象。好处是通用性极强坏处是返回的是动态对象没有静态类型提示字段访问容易拼错。我建议常用资源用静态客户端CRD 或小众资源用动态客户端不要在动态客户端里手写太多复杂逻辑。值得注意的是resources.get()这种方式每次都会做 Discovery 查询频繁调用有性能开销。可以缓存 Resource 对象获取一次后重复使用能省掉不少 API Server 的负载。5. 常见问题排查与性能优化速查这部分是实打实的避坑记录。很多问题不是代码逻辑错了而是对 K8s 的认证、鉴权、API 版本机制理解不到位。5.1 401 / 403 / 404三种报错的分诊思路这三种 HTTP 状态码在 K8s 客户端里几乎是每个新手都会遇到的我先给个速查表错误码含义最常见原因排查方向401 Unauthorized认证失败kubeconfig 里的 token 过期或者在集群内运行时 ServiceAccount token 无效先确认kubectl cluster-info是否能通检查 token 是否过期403 Forbidden鉴权失败当前身份没有对应资源的操作权限检查 RBAC Role/RoleBinding用kubectl auth can-i自查404 NotFound资源不存在apiVersion 或 kind 写错、资源确实不存在、API 版本已废弃用kubectl api-resources确认资源版本先说 401。在本地用 kubeconfig 连接时最常见的场景是登录凭证过期。你可以先用kubectl get pods命令自测一下如果命令能通而 Python 报 401基本可以排除集群本身的问题剩下的就是客户端加载的 kubeconfig 和当前 context 对不对。我强烈建议在脚本开头加一行调试输出确认加载的集群地址import os context_name os.getenv(KUBECONFIG, ~/.kube/config) print(fUsing kubeconfig: {context_name})403 是另一个极端它的常见场景是脚本跑在 Pod 里但 Pod 的 ServiceAccount 权限不够。默认的defaultServiceAccount 通常只有很少的权限。你以为代码没问题其实是被 RBAC 拦住了。排查手段kubectl auth can-i list pods --assystem:serviceaccount:default:default这条命令可以直接返回 yes 或 no比你翻代码快得多。404 前面已经详细说过多半是 apiVersion 和 kind 不匹配。特别提醒v1这个版本是 core group 专用的其他 group 千万不要写v1一定要写成apps/v1、batch/v1、monitoring.coreos.com/v1这种完整形式。5.2 大规模查询的性能优化分页、过滤与并发如果你的脚本需要一次性处理几千个 Pod直接list_pod_for_all_namespaces()会把所有对象全量拉回来网络开销和内存开销都很高。API Server 也扛不住你这样折腾。正确的做法是分页。Python 客户端的 list 接口支持limit和_continue参数pod_list [] continue_token None while True: resp v1.list_pod_for_all_namespaces( limit500, _continuecontinue_token ) pod_list.extend(resp.items) if not resp.metadata._continue: break continue_token resp.metadata._continue_continue参数会根据上一次返回的 metadata 自动生成相当于一个下一页的游标。配合limit500可以把一个大列表拆成多次小请求。过滤也是省请求的好办法。field_selector和label_selector是两把利器# 只查 Pending 状态的 Pod避免全部拉取 pending_pods v1.list_pod_for_all_namespaces( field_selectorstatus.phasePending ) # 按标签查比如只查 appnginx 的 Pod nginx_pods v1.list_namespaced_pod( namespacedefault, label_selectorappnginx )并发优化要谨慎。Python 的 GIL 决定了多线程做纯计算没用但网络请求是 IO 密集型的多线程能明显提速。我通常用ThreadPoolExecutor做并发调用from concurrent.futures import ThreadPoolExecutor def get_pod_count(namespace): return len(v1.list_namespaced_pod(namespacenamespace).items) with ThreadPoolExecutor(max_workers8) as executor: counts list(executor.map(get_pod_count, [default, kube-system, monitoring]))提升明显但并发数别开太大。8 到 16 是很稳的范围太大会触发 API Server 的限流。5.3 网络超时与重试策略客户端调 API Server本质上还是 HTTP 请求网络抖动是绕不开的。生产环境里我吃过亏一个批量脚本跑一半某一次 list 请求因为网络超时挂掉整个任务失败重来。后来统一加了重试逻辑。重试不是无脑重发。我的策略是对 429请求过多和 5xx服务端错误重试对 4xx 不重试——4xx 是请求本身有问题重试一万次也一样。加上指数退避避免对 API Server 造成二次伤害import time from kubernetes import client def call_with_retry(func, *args, retries3, **kwargs): for i in range(retries): try: return func(*args, **kwargs) except client.exceptions.ApiException as e: if e.status 500 or e.status 429: wait 2 ** i print(fHTTP {e.status}, retry in {wait}s) time.sleep(wait) else: raise超时设置也要显式配置。默认情况下HTTP 请求的超时时间由底层 urllib3 控制有时候一挂就是几分钟。你可以用client.Configuration设置超时configuration client.Configuration() configuration.timeout 30 api_client client.ApiClient(configuration)这个设置对同步调用很有效。遇到极端慢的 API Server与其无限等不如快速失败然后由重试机制接管。6. 日志调试与最后的自留技巧最后再分享一个非常实用但很多人不知道的调试技巧打开客户端库的 Debug 模式。from kubernetes import client config.load_kube_config() client.Configuration().debug True打开之后客户端会把每个 HTTP 请求的详细信息打印出来包括请求 URL、Header、响应状态码。这在你排查 401、隔离 RBAC 问题时是神器。比如你可以清楚地看到某个操作请求的到底是/apis/apps/v1还是/api/v1是不是自己路径写错了。注意这只是调试手段生产环境开着反而会导致日志刷屏。根据我个人把这些 API 串联起来写运维工具的经验最大的体会是先把要操作的资源属于哪个 API Group这件事想清楚再去查这个 Group 对应哪个 Python API 对象最后动手写代码比一上来就对着方法名猜要快得多也少踩很多坑。这套能力沉淀下来之后你会发现它不只是省掉了敲 kubectl 的时间更重要的是所有操作都变成了可追溯、可重试、可并发、可集成的程序逻辑。把这些常用 API 串成一个内部工具链日常巡检、发版、清理都能自动化那才是 Kubernetes 真正的工程化体验。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →