K8s 1.36 ImageVolume GA:OCI 镜像不再只能跑容器

OCI 规范建立的目的就是将容器镜像格式标准化,正如其名,OCI 镜像在之前一直用来跑容器,但现在它还能干更多事。
Kubernetes v1.36 里 ImageVolume 特性正式 GA 了,该特性允许我们把 OCI 镜像直接作为 Volume 挂载到 Pod 里。现在 OCI 镜像在 K8s 里不再只能跑容器了,模型权重、配置文件、安全签名、CI/CD 工件,只要是只读数据,都可以打包成 OCI 镜像供 Pod 挂载使用。
这个特性从 v1.31 以 Alpha 引入,经过 v1.33、v1.35 的 Beta 阶段,终于在 v1.36 正式 GA。GA 之后不用再手动开 Feature Gate,containerd 也原生支持了,开箱即用。
1. 背景
OCI(Open Container Initiative)是 2015 年在 Linux 基金会支持下成立的开放项目,Docker、CoreOS(后来被 Red Hat 收购)和容器行业的主要厂商一起,围绕容器格式和运行时制定了开放标准。Docker 捐出了自己的镜像格式作为基础,社区在此基础上逐步形成了 Runtime、Image 和 Distribution 三大规范。
2017 年 image-spec v1.0 发布,容器镜像的格式算是定下来了。在 OCI 标准下,运行一个容器的过程就是:从 Registry 下载 OCI 镜像 → 解压到 OCI Bundle → OCI Runtime 运行这个 Bundle。整个流程标准化之后,不同 Runtime、不同 Registry 之间可以互操作,不用强行绑定 Docker。
Docker 也把 libcontainer 的实现移动到 runC 捐给了 OCI,社区有了第一个 OCI Runtime 的参考实现。
OCI Artifacts
虽然 OCI 标准最初完全是围绕容器设计的,镜像格式里的 mediaType、config 结构都是为「跑容器」量身定做的。
但是如果我不想跑容器,只想把一堆文件打包分发呢?人们很快发现,OCI Registry 的分层存储和分发机制天然适合分发更多东西。
OCI 镜像的本质是什么?就是一堆只读的层(layer),加上一个 manifest 描述这些层的组织方式,再通过 Registry 的 API 完成分发。同时 OCI Registry 提供了一整套“可寻址、可校验、可去重、可控访问”的分发原语,然后这个模型并不绑定「容器」。
所以社区很自然地想到:能不能把 OCI Registry 当成一个通用的内容分发平台来用?
实际上社区很早就开始在 OCI Registry 里存非镜像内容了,但早期的做法都是 hack——把非镜像内容伪装成容器镜像塞进去,Registry 其实并不知道这些东西不是用来跑的:
- Helm 从 3.0 开始支持把 Chart 推到 OCI Registry,算是最早的「Registry 当通用存储」的生产实践。
- Cosign 直接把容器签名、SBOM 也存进 OCI Registry,用镜像层来承载签名数据。
- ORAS(OCI Registry As Storage)更猛,WASM 模块、OPA 策略、Falco 规则都能往里塞,相当于把 OCI Registry 当成一个通用的对象存储来用。
OCI Artifacts 就是这么来的,把各种产物存进 OCI Registry、当成通用内容仓库来分发。
这些用法推动了 OCI 规范本身的演进。2024 年,image-spec v1.1.0 正式加入了 artifactType 字段,允许 Manifest 声明「我不是容器镜像,我是一个签名 / 一个 Helm Chart / 一个模型权重」。OCI 对非镜像内容的支持从社区 hack 变成了规范的一部分,OCI Registry 正式成为了一个通用的内容仓库。

K8s ImageVolume
现在 OCI Registry 已经变成了一个通用的内容仓库,但问题来了:Helm、Cosign、ORAS 这些工具都在往里塞东西,但到了 Kubernetes 这边,OCI 镜像还是只能拿来跑容器,缺少一个原生的消费方式。
ImageVolume 就是来补上这一块的。它允许在 Pod 中将 OCI 镜像直接作为 Volume 挂载,让 OCI Artifacts 在 K8s 里也能被原生消费,不再只是跑容器。就像这样:
| |

不过有一点要注意,ImageVolume 挂载是只读的,如果需要在运行时修改挂载的文件,还是得用 PVC。后面会详细讨论这个限制。
ImageVolume 让 OCI Artifacts 在 K8s 里有了第一个原生的消费方式。不过这个能力并不是一步到位的,从 Alpha 到 GA 走了近两年。
2. ImageVolume 从 Alpha 到 GA
ImageVolume 这个特性来源于 KEP-4639,由 SIG Node 和 SIG Storage 共同推动。从 v1.31 Alpha 到 v1.36 GA 走了近两年,具体时间线如下:
| 阶段 | K8s 版本 | 发布时间 | Feature Gate 默认值 | 说明 |
|---|---|---|---|---|
| Alpha | v1.31 | 2024-08 | false | 需要手动开启 Feature Gate |
| Beta(默认关) | v1.33 | 2025-04 | false | Beta 代码合入但仍默认关闭 |
| Beta(默认关) | v1.34 | 2025-08 | false | 移除 noexec 限制,仍默认关闭 |
| Beta(默认开) | v1.35 | 2025-12 | true | 首次默认启用 |
| GA | v1.36 | 2026-04 | true(锁定) | Feature Gate 锁定,v1.39 移除 |
Containerd v2.1.0 才正式支持 ImageVolume,而且没有回移到 v2.0.x 分支,所以用 containerd 的话必须升级到 v2.1.0+。

接下来过一遍每个阶段的变化。
2.1 subPath 支持(Alpha → Beta)
Alpha 阶段 ImageVolume 不支持 subPath,也就是说你只能挂载镜像的整个文件系统,没法只挂载其中的某个子目录。
Beta 阶段(v1.33)解除了这个限制,subPath 和 subPathExpr 都可以用了。对应的 CRI API 也新增了 image_sub_path 字段来支持这个功能。
现在你可以这样用:
| |
很多时候一个镜像里会放多个目录,有了 subPath 就不用把整个镜像都挂进来了。
2.2 noexec 限制移除(Alpha → Beta)
Alpha 阶段 ImageVolume 挂载时强制加了 noexec 选项,挂载进来的文件不能被执行。
这个限制在 Beta 阶段(2025-06,PR #5354)被移除了。社区讨论后觉得 noexec 限制过于严格,ImageVolume 的主要用途是分发只读数据,强制 noexec 没有必要,反而限制了某些合理的使用场景,比如挂载包含可执行工具的镜像。
不过 ImageVolume 仍然是只读挂载(ro),读写支持还得等后续的 KEP。
2.3 Kubelet 监控指标(Alpha → Beta)
Beta 阶段新增了 3 个 Kubelet 指标,方便监控 ImageVolume 的使用情况。
kubelet_image_volume_requested_total— 请求的 ImageVolume 数量kubelet_image_volume_mounted_succeed_total— ImageVolume 挂载成功的数量kubelet_image_volume_mounted_errors_total— ImageVolume 挂载失败的数量
GA 阶段这些指标提升到了 BETA 稳定性级别,可以在 Prometheus 里配告警了。
2.4 Feature Gate 锁定(GA)
v1.36 GA 后,ImageVolume Feature Gate 被锁定为默认开启,没法关了。按照 K8s 的惯例,Feature Gate 会在 GA 后 3 个版本移除,也就是 v1.39 会彻底删掉这个 Gate。
所以现在的状态就是:
- 不再需要手动开启 Feature Gate 了,v1.36 集群开箱即用
- API 字段上的
+featureGate=ImageVolume注解也被移除了 - E2E 测试提升为 Conformance 级别,这是 GA 的标志之一
2.5 containerd 原生支持
这个虽然不是 K8s 代码的变化,但可能是对使用者影响最大的变化。
Alpha 阶段,containerd 不支持 ImageVolume,想玩的话只能自己动手。参考我之前的文章 K8s V1.31 新特性:ImageVolume,
我当时手动 checkout 了 containerd 的 PR #10579,编译替换二进制文件。编译倒是不难,但是那个 PR 还有个 bug,kubelet 没有把 readOnly 参数透传到 CRI mounts 配置中,导致 containerd 校验 readOnly 失败一直报错。。。没办法,还得手动注释掉校验逻辑,整个流程折腾下来真的挺崩溃的。
现在 containerd v2.1.0 已经原生支持 ImageVolume,直接用就行,不需要任何 hack。
CRI-O 的话从 v1.31 就支持了,v1.34 还增加了 subPath 支持,一直走在前面。
2.6 变化总结
整理一下:
| 变化项 | Alpha (v1.31) | Beta (v1.33-v1.35) | GA (v1.36) |
|---|---|---|---|
| subPath | ❌ 不支持 | ✅ 支持 | ✅ 支持 |
| noexec 限制 | 强制 noexec | 移除 | 无限制 |
| 监控指标 | 无 | Alpha 级别 | BETA 级别 |
| Feature Gate | 默认关,需手动开 | v1.33/34 默认关,v1.35 默认开 | 锁定开启 |
| containerd | 需手动编译 PR | v2.1.0 原生支持 | v2.1.0+ |
| CRI-O | v1.31 支持 | v1.34 支持 subPath | 同左 |
| 挂载模式 | 只读 | 只读 | 只读 |
3. 现在怎么用
变化聊完了,实际用起来是什么感觉呢。GA 之后用起来比 Alpha 阶段简单太多了,不用再折腾 Feature Gate 和手动编译 containerd 了。
3.1 环境要求
- Kubernetes >= v1.36
- Container Runtime:
- containerd >= v2.1.0
- CRI-O >= v1.31(subPath 需要 >= v1.34)
就这么简单,不需要额外配置任何 Feature Gate。
本次验证环境是使用 KubeClipper 安装的 K8s 集群,版本如下:
- Kubernetes v1.36.1
- containerd v2.2.4
3.2 构建目标镜像
使用方式和 Alpha 阶段基本一致。先构建一个包含模型文件的 OCI 镜像,用 FROM scratch 就行,不需要任何基础镜像。
为了后面演示 subPath,这里在镜像里放两个模型目录,再放一个配置文件:
| |
目录结构如下:
| |
Dockerfile 如下:
| |
构建并推送到镜像仓库:
| |
3.3 基本挂载
创建 Pod 挂载这个镜像:
| |
应用到集群,等 Pod Running 后查看挂载内容:
| |
镜像里的文件都挂载进来了,跟预期一致。
3.4 subPath 挂载
上面那个镜像里放了两个模型目录,如果 Pod 只需要 Qwen2-0.5B,不需要把整个镜像都挂进来,用 subPath 就行:
| |
验证一下,挂载目录里只有 Qwen2-0.5B 的内容:
| |
只挂载了 Qwen2-0.5B 目录,Llama2-7B 和 app.conf 都不在。如果 subPath 指定的路径在镜像中不存在,容器创建会直接报错:
| |
3.5 只读挂载验证
ImageVolume 挂载是只读的,尝试写入会报 Read-only file system:
| |
最后提几个实际使用中的注意事项。
ImageVolume 挂载是只读的,如果需要运行时修改文件还是得用 PVC,目前没有读写支持的 KEP。Pod 重建时 ImageVolume 会重新解析远端镜像,所以生产环境建议用 digest 而不是 tag 引用镜像,避免 Pod 重建后 tag 被覆盖导致拿到非预期版本。
镜像层共享能省磁盘,两个 ImageVolume 引用的镜像有相同层的话 containerd 只存一份,但大模型镜像多了也要注意节点磁盘压力。
4. 小结
OCI 从 2017 年 image-spec v1.0 发布到今天,走了一条挺清晰的路:先是容器镜像格式标准化,然后 OCI Registry 被社区 hack 成通用内容仓库,接着 image-spec v1.1.0 和 distribution-spec v1.1.0 把这种用法正式写入规范(artifactType + referrers API)。
OCI Artifacts 从社区变通变成了标准的一部分。ImageVolume GA 等于 Kubernetes 也跟上了,OCI Artifacts 在 K8s 里有了第一个原生的消费方式。
往后看,读写支持可能是下一个方向,目前只读挂载还是限制了些场景,社区已经有相关讨论。另外 ImageVolumeWithDigest 子特性在 v1.36 也 GA 了,Pod Status 里能直接看到挂载镜像的 digest,版本追溯方便不少。