Skip to content

Kubernetes ​

一个 Go 服务做成镜像后,还需要回答几件事:进程崩了谁重启,机器坏了谁补副本,新版本怎样逐步接流量,容量不够时该扩 Pod 还是扩机器。Kubernetes 把这些事情拆成可以独立观察、反复协调的 API 对象。学它时最有用的顺序,是顺着一次请求和一次发布,把这些对象接起来。

本文覆盖 Pod、Deployment、StatefulSet、Service、Ingress 与 Gateway API,接着讲配置、探针、资源、HPA、滚动更新,最后补上调度、网络和 Operator。官方当前文档与发布页在 2026-10-13 显示 1.37 系列;正文使用成熟 API,不把新特性开关视为所有集群默认配置。本次没有 Kubernetes 集群或 kubectl:实际执行的是 Python 3.12.14、PyYAML 6.0.3 的本地清单解析、标签检查和数值模型。它们不能证明 API 接受、调度成功或真实网络可达。

“声明三个副本”以后,究竟是谁动手 ​

API 对象的 spec 表达期望,status 表达系统观察到的状态。提交成功只表示请求已被接受,不能直接翻译为“应用发布成功”。控制器不断比较两者,执行一步动作,再继续观察。组件说明

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart TB
    U[提交 Deployment:期望 3 副本] --> A[API server:认证、授权、准入]
    A --> E[etcd 保存对象]
    A --> DC[Deployment controller]
    DC --> RS[ReplicaSet controller 创建 Pod]
    RS --> A
    A --> SC[Scheduler 为未绑定 Pod 选择节点]
    SC --> A
    A --> KL[kubelet 观察分配给本节点的 Pod]
    KL --> CR[容器运行时创建容器]
    KL --> A
    A --> ST[status、conditions、events]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart TB
    U[提交 Deployment:期望 3 副本] --> A[API server:认证、授权、准入]
    A --> E[etcd 保存对象]
    A --> DC[Deployment controller]
    DC --> RS[ReplicaSet controller 创建 Pod]
    RS --> A
    A --> SC[Scheduler 为未绑定 Pod 选择节点]
    SC --> A
    A --> KL[kubelet 观察分配给本节点的 Pod]
    KL --> CR[容器运行时创建容器]
    KL --> A
    A --> ST[status、conditions、events]

API server 是状态入口,etcd 保存控制面状态;scheduler 选节点,kubelet 在选中的节点上与容器运行时合作。Deployment controller 不直接调用某台机器上的 Docker 来跑进程。了解这个分工以后,排错可以顺着链走:对象没创建看准入与权限,Pod Pending 看调度事件,已分配但起不来看镜像拉取、挂载与容器状态。

控制循环的好处是可以持续修正偏差,代价是最终收敛需要时间。控制器、网络、指标采集都各有延迟,所以“声明式”不能解释成一次操作后的即时事务。

Pod 是共同生活的单位,Deployment 才是可替换副本的管理者 ​

一个 Pod 里可以有多个容器,它们共享网络空间和 IP,能通过 localhost 通信;卷是否共享,要看各容器有没有挂载相同卷。Pod 内的普通容器并不因此默认共享 PID namespace。需要紧密配合、共同调度的进程才适合放在一起。Pod

需要管理什么选什么得到什么自己仍要负责什么
单个运行实例的组成Pod容器、卷、网络和生命周期边界单独创建的 Pod 不是完整自愈发布方案
可互换的 HTTP API 副本DeploymentReplicaSet、扩缩容、版本滚动替换无状态设计、就绪语义、数据库兼容
稳定身份及独立持久卷StatefulSet有序编号、稳定身份、每副本 PVC数据复制、选主、备份、恢复与一致性
每个合适节点一个代理DaemonSet随节点安排副本节点差异与资源占用
做完就退出的任务Job / CronJob完成重试或按时产生 Job幂等、外部副作用、任务超时

Pod 内容器重启,不代表换了 Pod;节点失联后控制器创建替代 Pod,则是另一个 UID 的对象。Pod 通常不会被“搬”到另一节点继续保持原进程内存。因此 session、订单进度、关键缓存恢复信息不能只存在单个进程里。Pod 生命周期

Deployment 用 label selector 找自己的 Pod 模板。下面三个位置里的 app: api 必须建立正确关系:Deployment selector、Pod template labels、Service selector。它们不要求所有标签完全相同,但 selector 的每个条件必须匹配。

yaml
selector:
  matchLabels:
    app: api
template:
  metadata:
    labels:
      app: api

本地检查读取了文末完整清单,并故意把反例里的模板标签改为 app: wrong:

text
YAML parsed: 5 objects; selectors: matched
negative fixture: selector mismatch detected

第一项成功只表明五段 YAML 能解析且这几组选择器相符。它没有检查 Kubernetes 所有字段,更没有绕过 API server 的 schema、默认值、准入策略或版本要求。

StatefulSet 带来的是稳定身份,不是现成数据库 ​

假设三个副本叫 db-0、db-1、db-2。StatefulSet 可以让替代 db-1 的 Pod 继续使用相应身份和 PVC;headless Service 为这些成员提供发现入口,volumeClaimTemplates 为各成员申请独立持久卷。默认 OrderedReady 管理带有顺序约束,更新也有自己的顺序与 partition 控制,并不是 Deployment 的 maxSurge 算法。StatefulSet

稳定的名字不会替数据库执行复制协议。三份空磁盘上的数据库不会因为 replicas=3 自动形成高可用集群;存储类、可用区、访问模式、卷重挂载速度也会限制恢复。PVC 默认保留行为和可配置的保留策略、PV reclaim policy 必须一起检查,不能以“删除 StatefulSet 不删数据”替代备份与恢复演练。若团队不准备维护这些责任,托管数据库往往更适合应用团队。

请求怎样找到会变化的 Pod ​

Service 把一个相对稳定的访问入口与一组变化的后端联系起来。selector 匹配 Pod,EndpointSlice 表达后端地址和状态,具体数据面把连接转发过去。Service 名称可由集群 DNS 解析;ClusterIP 通常是虚拟服务地址,不是一台真正的代理机器。Service

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    C[客户端] --> G[入口控制器或 Gateway 数据面]
    G --> S[Service api:80]
    S --> ES[EndpointSlice 中的可用端点]
    ES --> P1[Pod A:8080]
    ES --> P2[Pod B:8080]
    D[Service selector] -. 选择 .-> P1
    D -. 选择 .-> P2
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    C[客户端] --> G[入口控制器或 Gateway 数据面]
    G --> S[Service api:80]
    S --> ES[EndpointSlice 中的可用端点]
    ES --> P1[Pod A:8080]
    ES --> P2[Pod B:8080]
    D[Service selector] -. 选择 .-> P1
    D -. 选择 .-> P2

这是一张逻辑关系图。某些入口控制器会直接读取 EndpointSlice 并连 Pod,不逐包经过 ClusterIP;实际路径要看实现。Service 也不保证 HTTP 请求级均匀分配,长连接可能持续落在同一后端。

类型或入口常见用途边界
ClusterIP Service集群内部稳定发现不等于公网入口
Headless Service直接发现多个成员地址clusterIP: None,不提供通常的虚拟 IP 负载均衡
NodePort Service通过节点端口提供入口节点暴露面、路由、防火墙要另行控制
LoadBalancer Service请求基础设施提供负载均衡器需要实现支持,可能创建收费资源
IngressHTTP/HTTPS 域名、路径路由必须有 Ingress controller;注解常有实现差异
Gateway API以角色和类型拆分网关、路由等能力必须安装相应 CRD 与支持所需能力的实现

Ingress API 已冻结,仍可使用;新能力主要在 Gateway API 演进。GatewayClass 表示控制器类别,Gateway 表示监听器及基础设施入口,HTTPRoute 表示业务路由。这样平台团队可管理入口,应用团队管理被授权关联的路由。跨命名空间引用还要受授权边界控制,不能因为能写 HTTPRoute 就访问任意 Secret 或后端。Ingress、Gateway API

选择时先检查实现的 conformance 与已支持字段,而不是看 YAML 的 apiVersion 好看就认为所有控制器行为相同。TLS 在入口终结后,入口到 Pod 是否仍加密,是另一项配置;不能用外侧 HTTPS 证明整条链都加密。

配置和秘密,为什么要分开管理 ​

ConfigMap 适合不敏感的设置,Secret 用于机密数据的 API 表达与分发。两者都能通过环境变量或卷交给应用,但更新语义不同:环境变量通常要新进程才能读取新值;卷投射可以异步更新,应用仍须主动重新读取,subPath 挂载还有更新限制。ConfigMap

本文把 ConfigMap 命名为 api-config-v1 并设置 immutable,更新时创建新名字,再改 Pod 模板引用。这样配置变更进入滚动发布与审计链,代价是需要清理不再引用的旧对象。单纯修改同名 ConfigMap 不一定触发 Deployment rollout,因为 Pod template 没变。

Secret 中的 base64 是编码,拿到内容的人能解码。使用 Secret 还要配置 etcd 静态加密、访问权限、审计、轮换及应用的读取范围。能在某命名空间创建 Pod 的人,可能通过挂载读取该空间的 Secret;仅限制直接 get secrets 并不足以建立完整隔离。Secret 安全说明

清单不应带真实密码进入 Git。本例不需要访问 Kubernetes API,故设 automountServiceAccountToken: false;真正的控制器才按需要配 ServiceAccount 与最小 RBAC。应用访问云资源时,也应考虑 workload identity 或短期身份,而不是把长期密钥烘焙进镜像。

三种探针,失败后的动作完全不同 ​

探针回答的问题失败会怎样常见错误
startupProbe初始化完成了吗连续失败达到阈值后按重启策略处理容器慢启动却只把 liveness 放宽到很迟钝
readinessProbe现在适合接新流量吗影响端点就绪状态,通常停止常规新流量误认为失败就一定重启容器
livenessProbe进程是否陷入可由重启恢复的故障连续失败达到阈值后重启容器把共享数据库短暂不可用当进程死锁

配置 startupProbe 后,成功前不会运行 readiness 和 liveness。HTTP 探针主要判断返回状态,TCP 探针主要判断能否连接;“端口能接上”不保证业务数据正确。探针文档

例如数据库短时故障会同时影响所有副本。若 liveness 每次都查数据库,所有 Pod 可能一起重启,重连又给数据库增加压力。更稳妥的划分是让 liveness 检查本进程是否有恢复意义,readiness 根据业务能否提供有用响应决定接不接新流量;是否把某个下游故障纳入 readiness,要看服务是否有降级能力。

探针是周期性观测,EndpointSlice、代理和负载均衡器还需要传播时间。正在处理的连接也不会因为一次 readiness 失败就自动优雅结束。因此退出必须与应用的停止接单、在途请求等待、terminationGracePeriodSeconds 一起设计。

requests 影响能否放进去,limits 影响运行时约束 ​

本文使用传统的每容器 CPU、内存配置,不依赖 Pod-level resource 等版本相关能力:

yaml
resources:
  requests:
    cpu: 500m
    memory: 128Mi
  limits:
    cpu: '1'
    memory: 256Mi

500m 是半个 CPU 的时间需求,不是 500 毫秒;128Mi 是 128×2²⁰ 字节。scheduler 主要用 requests 做容量安排,所以节点眼前很空闲,也可能因为已承诺的 requests 没剩多少而无法再调度。运行时在有余量时可以超过 request;CPU limit 通常通过节流约束平均 CPU 时间,内存 limit 则涉及内核回收和 OOM。资源管理,固定 1.36 文档

配置选择好处可能的代价
request 接近经验证的稳定需求更可靠地预留调度容量填得过大降低集群利用率
request 很小更容易挤进节点争用时表现变差,HPA 百分比也会被放大
较紧的 CPU limit限制干扰与成本边界突发计算被节流,尾延迟可能升高
较紧的 memory limit控制单容器失控范围正常峰值也可能 OOM,无法靠“稍慢一点”完成

这里“预留”是调度与资源分配语义,不等于独占物理核心或为进程预先分配同等大小的 RAM。独占 CPU 等行为还取决于 QoS、CPU Manager 策略和节点设置。Go 的 GOMAXPROCS 与 GOMEMLIMIT 需要在这层约束之内再协调,详见《Go 在容器中》。

HPA 想扩六个副本,不代表马上就有六个 ​

HPA 读取指标,调整目标工作负载的副本数。最简直觉公式为:当前副本数乘以“当前指标 / 目标指标”,再向上取整。CPU 的 Utilization 以 request 为分母;不填合适的 request,百分比本身就可能不可用或失真。HPA 算法

python
desired = math.ceil(3 * 140 / 70)
slots = (2000 - 500) // 500
text
HPA idealized target: 6; free CPU-request slots: 3

这不是集群读数。模型假设当前 3 个副本、CPU 利用率 140%、目标 70%;另一独立容量场景假设节点可分配 2000m,已经承诺 500m,每个新 Pod 请求 500m,所以还剩 3 个 CPU-request 槽位。真实 scheduler 还要看内存、卷、污点、亲和性和拓扑,并不能只做这个除法。

真实 HPA 还处理容忍区间、缺失指标、未就绪 Pod、扩缩速率和稳定窗口。多个指标时会综合建议,并不是任意一个指标低就缩容。扩 Pod 之后还可能等待拉镜像、启动、就绪;节点不够则需要独立的节点扩容机制,HPA 自己不创造机器。扩容也可能把数据库连接数放大,所以要同时限制连接池和下游并发。

CPU HPA 还有一个先决条件:集群必须提供可用的 metrics.k8s.io 资源指标 API,通常由另外安装的 Metrics Server 提供。它不是创建 HPA 对象就自动出现的服务。指标缺失、采集失败或 API 不可用时,控制器无法据此得出上面的扩容建议;应查看 HPA 的 conditions、events 和资源指标 API,而不只检查 Deployment。CPU Utilization 还要求相关容器设置 CPU request:没有定义 request 时,相应 Pod 的利用率可能无法定义,不能被当成一个正常的百分比样本,HPA 也不能照着本文的理想公式执行。自定义与外部指标则需要相应适配器和 API。HPA 的指标 API 与算法前提

启用 HPA 后,还要决定谁拥有 replicas 字段。GitOps 每次同步都强制写回固定 replicas,可能与 HPA 互相覆盖;应按所用工具配置字段所有权或忽略规则,并保留 min/max 与审批边界。

滚动更新先保留多少旧副本,再增加多少新副本 ​

Deployment 的 RollingUpdate 用 maxUnavailable 控制允许的不可用数,用 maxSurge 控制相对期望副本数可以额外创建的数。百分比换算时前者向下取整,后者向上取整。Deployment 更新策略

text
replicas=1: 25% surge=1 unavailable=0
replicas=3: 25% surge=1 unavailable=0
replicas=4: 25% surge=1 unavailable=1

这三行是本地数字模型输出,不是 rollout 的实测。它说明小副本数时,默认百分比可能与直觉不同。本文显式设 maxSurge: 1、maxUnavailable: 0,表达先多起一个、等可用后再减旧副本的意图。若没有空间容纳额外副本,新 Pod 会 Pending,发布可能停住。

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
stateDiagram-v2
    [*] --> OldServing: 3 个旧副本可用
    OldServing --> NewStarting: 创建 1 个新副本
    NewStarting --> NewReady: startup/readiness 成功
    NewReady --> NewAvailable: 满足 minReadySeconds
    NewAvailable --> ReplaceOne: 减少旧副本
    ReplaceOne --> NewStarting: 还有旧副本
    ReplaceOne --> Done: 全部替换
    NewStarting --> Stalled: 镜像、调度或探针失败
    Stalled --> NewStarting: 人工修正或控制器重试
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
stateDiagram-v2
    [*] --> OldServing: 3 个旧副本可用
    OldServing --> NewStarting: 创建 1 个新副本
    NewStarting --> NewReady: startup/readiness 成功
    NewReady --> NewAvailable: 满足 minReadySeconds
    NewAvailable --> ReplaceOne: 减少旧副本
    ReplaceOne --> NewStarting: 还有旧副本
    ReplaceOne --> Done: 全部替换
    NewStarting --> Stalled: 镜像、调度或探针失败
    Stalled --> NewStarting: 人工修正或控制器重试

progressDeadlineSeconds 到期会报告进展失败,不等于自动回滚。kubectl rollout status、describe deployment、Pod events 和应用错误率应一起看。rollout undo 能恢复旧 Pod 模板,却不会撤销已经执行的数据库迁移;发布前用扩展再收缩的兼容变更,通常比期待回滚逆转数据更可靠。

正在终止的 Pod 还会占资源,因此“副本数加 maxSurge”不是任何时刻物理资源占用的绝对上限。PodDisruptionBudget 主要约束经过 eviction 的自愿中断,不保证机器故障时一定有足够副本,也不替代 Deployment 自身的滚动策略。终止与中断

调度和网络:集群能跑起来的两组前提 ​

scheduler 先过滤不适合的节点,再为可行节点评分并绑定 Pod。常用入口各有不同力度:scheduler

控制手段作用误用后果
nodeSelector / required node affinity硬性节点条件没有匹配节点就一直 Pending
preferred affinity在可行节点里表达偏好不是必须满足的承诺
taint / toleration节点拒绝未容忍的工作负载toleration 只是允许,不保证被安排到那里
topology spread / pod anti-affinity把副本分散到节点或区域过严且容量不足时无法调度
priority / preemption资源紧张时区分优先级被抢占服务的可用性会受影响

三个副本若都落在一台机器上,数量看似够,机器故障仍可能全失。因此高可用设计必须把副本数、故障域、资源容量和调度规则放在一起看。

Kubernetes 网络模型为 Pod 提供可路由的地址关系,具体 CNI 插件实现节点间连通。kube-proxy 或其他服务数据面实现 Service 转发,DNS 做名字发现。三者不是一回事:能解析 Service 名字但连不上,可能是端点、策略或数据面问题;Pod IP 能通而 Service IP 不通,则要继续查 Service 这一层。网络模型

NetworkPolicy 是允许规则的组合,需由支持它的网络插件执行。没有选中 Pod 的相关隔离策略时,不能假设默认网络已经零信任;一条 YAML 被 API 接受,也不能证明插件确实执行了它。实施 default-deny 时还要明确允许 DNS、必需的下游、探测等实际路径,并测试 ingress 与 egress。常见 NetworkPolicy 主要处理 L3/L4 条件,不自动验证 HTTP 用户身份或 TLS 证书。NetworkPolicy

CRD 定义新对象,Operator 把运维知识写成控制循环 ​

CRD 为 API server 增加自定义资源的类型、schema 和版本。仅注册一个 Database CRD,不会自动建数据库;还需要控制器观察这些对象,创建或更新 StatefulSet、Service,执行备份等领域动作。这样的应用领域自动化通常称为 Operator。Custom Resources、Operator

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    CR[Database 自定义资源 spec] --> R[Reconcile 读取当前事实]
    R --> D{与期望是否一致}
    D -->|有差异| A[执行可重复的一步动作]
    A --> O[观察结果并更新 status]
    O --> R
    D -->|一致| W[等待事件或定期重查]
    W --> R
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    CR[Database 自定义资源 spec] --> R[Reconcile 读取当前事实]
    R --> D{与期望是否一致}
    D -->|有差异| A[执行可重复的一步动作]
    A --> O[观察结果并更新 status]
    O --> R
    D -->|一致| W[等待事件或定期重查]
    W --> R

事件可能重复、合并,也可能在处理前状态已经变化,所以 Reconcile 应根据当前状态做幂等协调,而不是依赖“刚才一定发生了某一次创建事件”。外部动作更要设计幂等键:控制器在云资源创建成功、status 写回前崩溃,重试时不能再买一份资源。finalizer 可以在删除对象前安排外部清理,但控制器失效也可能让删除一直等待,因此要有可观察状态与恢复手册。

写 Operator 的收益,是让反复发生且规则明确的运维动作可审计、可恢复;代价是要长期维护权限、重试、升级、版本转换和删除语义。只有一个 Deployment 加几份配置时,Helm 或 Kustomize 往往足够。把所有 YAML 包装成 CRD,并不会自动增加可靠性。

从清单到可用服务,还差哪些验证 ​

文末清单展示 Namespace、不可变 ConfigMap、Deployment、Service、HPA 五类对象,镜像故意保留明显占位符。它要求应用监听 8080,并实现 /healthz 与 /readyz;这里没有提供或发布一个真实镜像。修改镜像后还要检查只读根文件系统是否适合应用、是否需要临时卷、资源是否有测量依据。

验证层本次状态真正部署前能排除的问题
YAML 语法、几组 selector已运行语法及本例标签错配
API schema 与 admission未运行版本、字段、配额、组织策略
scheduler 与镜像启动未运行节点容量、拉取权限、卷与启动依赖
Service / Gateway 数据面未运行端点发现、路由、TLS、网络策略
发布与故障演练未运行新旧兼容、退出、扩缩容、恢复时间

在有授权的测试集群中,可先明确 context,再做 server-side dry-run、差异检查,最后才 apply。server-side dry-run 仍会联系 API server 并经过相应权限、准入,不是纯本地检查。发布后的成功标准应是就绪、业务请求、错误率与容量都达到要求,而不是单独看到 configured。

依据 ​

完整复现文件 ​

以下文件放在同一目录。已有 Python 3.12 与 PyYAML 6.0.3 时,运行 sh verify.sh;需要安装依赖时可在自己的虚拟环境中使用 requirements.txt。这里不包含 apply 脚本,也不会联系集群。check.py 的规则刻意很小,不能当通用 Kubernetes validator 使用。

app.yaml ​

代码块 · 120 行 · 点击收起 / 展开
yaml
apiVersion: v1
kind: Namespace
metadata:
  name: teaching
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: api-config-v1
  namespace: teaching
immutable: true
data:
  LOG_LEVEL: info
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  namespace: teaching
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  minReadySeconds: 5
  progressDeadlineSeconds: 120
  template:
    metadata:
      labels:
        app: api
    spec:
      automountServiceAccountToken: false
      terminationGracePeriodSeconds: 30
      securityContext:
        runAsNonRoot: true
        runAsUser: 65532
        runAsGroup: 65532
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: api
          image: REPLACE_WITH_VERIFIED_IMAGE_DIGEST
          ports:
            - name: http
              containerPort: 8080
          envFrom:
            - configMapRef:
                name: api-config-v1
          resources:
            requests:
              cpu: 500m
              memory: 128Mi
            limits:
              cpu: '1'
              memory: 256Mi
          startupProbe:
            httpGet:
              path: /healthz
              port: http
            periodSeconds: 2
            failureThreshold: 30
          readinessProbe:
            httpGet:
              path: /readyz
              port: http
            periodSeconds: 2
            timeoutSeconds: 1
          livenessProbe:
            httpGet:
              path: /healthz
              port: http
            periodSeconds: 10
            timeoutSeconds: 1
            failureThreshold: 3
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: [ALL]
---
apiVersion: v1
kind: Service
metadata:
  name: api
  namespace: teaching
spec:
  selector:
    app: api
  ports:
    - name: http
      port: 80
      targetPort: http
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api
  namespace: teaching
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  minReplicas: 3
  maxReplicas: 6
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300

selector-bad.yaml ​

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-bad
spec:
  replicas: 1
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: wrong
    spec:
      containers:
        - name: api
          image: REPLACE_WITH_VERIFIED_IMAGE_DIGEST

check.py ​

代码块 · 39 行 · 点击收起 / 展开
python
"""Local YAML/selector checks only; NOT Kubernetes schema/admission validation."""
import math
from pathlib import Path
import yaml


def load(name):
    return list(yaml.safe_load_all(Path(name).read_text()))


def selector_error(dep):
    want = dep["spec"]["selector"]["matchLabels"]
    got = dep["spec"]["template"]["metadata"]["labels"]
    return any(got.get(k) != v for k, v in want.items())


def main():
    docs = load("app.yaml")
    dep = next(x for x in docs if x["kind"] == "Deployment")
    svc = next(x for x in docs if x["kind"] == "Service")
    assert not selector_error(dep)
    labels = dep["spec"]["template"]["metadata"]["labels"]
    assert all(labels.get(k) == v for k, v in svc["spec"]["selector"].items())
    assert selector_error(load("selector-bad.yaml")[0])
    print(f"YAML parsed: {len(docs)} objects; selectors: matched")
    print("negative fixture: selector mismatch detected")
    # Explicit teaching inputs, not readings from a cluster.
    desired = math.ceil(3 * 140 / 70)
    slots = (2000 - 500) // 500
    assert desired == 6 and slots == 3
    print(f"HPA idealized target: {desired}; free CPU-request slots: {slots}")
    for n in (1, 3, 4):
        print(f"replicas={n}: 25% surge={math.ceil(n*.25)} unavailable={math.floor(n*.25)}")
    c = dep["spec"]["template"]["spec"]["containers"][0]
    assert c["image"] == "REPLACE_WITH_VERIFIED_IMAGE_DIGEST"
    print("deployment blocked: image placeholder must be replaced")

if __name__ == "__main__":
    main()

requirements.txt ​

text
PyYAML==6.0.3

verify.sh ​

sh
#!/bin/sh
set -eu
cd "$(dirname "$0")"
python3 --version
python3 -c 'import yaml; print("PyYAML", yaml.__version__)'
python3 check.py

保留各书来源与许可 · 示例运行条件以正文为准