主题
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]
API server 是状态入口,etcd 保存控制面状态;scheduler 选节点,kubelet 在选中的节点上与容器运行时合作。Deployment controller 不直接调用某台机器上的 Docker 来跑进程。了解这个分工以后,排错可以顺着链走:对象没创建看准入与权限,Pod Pending 看调度事件,已分配但起不来看镜像拉取、挂载与容器状态。
控制循环的好处是可以持续修正偏差,代价是最终收敛需要时间。控制器、网络、指标采集都各有延迟,所以“声明式”不能解释成一次操作后的即时事务。
Pod 是共同生活的单位,Deployment 才是可替换副本的管理者
一个 Pod 里可以有多个容器,它们共享网络空间和 IP,能通过 localhost 通信;卷是否共享,要看各容器有没有挂载相同卷。Pod 内的普通容器并不因此默认共享 PID namespace。需要紧密配合、共同调度的进程才适合放在一起。Pod
| 需要管理什么 | 选什么 | 得到什么 | 自己仍要负责什么 |
|---|---|---|---|
| 单个运行实例的组成 | Pod | 容器、卷、网络和生命周期边界 | 单独创建的 Pod 不是完整自愈发布方案 |
| 可互换的 HTTP API 副本 | Deployment | ReplicaSet、扩缩容、版本滚动替换 | 无状态设计、就绪语义、数据库兼容 |
| 稳定身份及独立持久卷 | 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
这是一张逻辑关系图。某些入口控制器会直接读取 EndpointSlice 并连 Pod,不逐包经过 ClusterIP;实际路径要看实现。Service 也不保证 HTTP 请求级均匀分配,长连接可能持续落在同一后端。
| 类型或入口 | 常见用途 | 边界 |
|---|---|---|
| ClusterIP Service | 集群内部稳定发现 | 不等于公网入口 |
| Headless Service | 直接发现多个成员地址 | clusterIP: None,不提供通常的虚拟 IP 负载均衡 |
| NodePort Service | 通过节点端口提供入口 | 节点暴露面、路由、防火墙要另行控制 |
| LoadBalancer Service | 请求基础设施提供负载均衡器 | 需要实现支持,可能创建收费资源 |
| Ingress | HTTP/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: 256Mi500m 是半个 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) // 500text
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: 人工修正或控制器重试
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
事件可能重复、合并,也可能在处理前状态已经变化,所以 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。
依据
- 组件、Pod、生命周期
- Deployment、StatefulSet、中断与 PDB
- Service、Ingress、Gateway API
- ConfigMap、Secret、探针
- 资源管理,v1.36、HPA
- 调度器、网络模型、NetworkPolicy
- CRD 与自定义资源、Operator、发布版本。查阅日期 2026-10-13。
完整复现文件
以下文件放在同一目录。已有 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: 300selector-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_DIGESTcheck.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.3verify.sh
sh
#!/bin/sh
set -eu
cd "$(dirname "$0")"
python3 --version
python3 -c 'import yaml; print("PyYAML", yaml.__version__)'
python3 check.py