OpenSandbox 实战:在 Kubernetes 中挂载 PVC 持久化卷,让 AI Agent 沙箱数据跨生命周期留存
【免费下载链接】OpenSandboxSecure, Fast, and Extensible Sandbox runtime for AI agents.项目地址: https://gitcode.com/GitHub_Trending/ope/OpenSandbox
导读
本指南以 OpenSandbox 仓库中的 kubernetes-pvc-volume-mount 示例 与配套文档 docs/examples/kubernetes-pvc-volume-mount.md 为主体,系统讲解如何把 Kubernetes PersistentVolumeClaim(PVC)挂载进 OpenSandbox 沙箱,使写入的数据在沙箱销毁后依然存在,供后续沙箱复用。你将掌握「自带 PVC(Bring Your Own)」「服务端托管持久化」「服务端托管临时存储」三种模式的选型与配置、Pool 模式下共享 PVC 的预挂载方式,以及 PVC 生命周期清理的底层机制,并能在自己的 Kubernetes 集群中直接跑通完整示例。
背景:为什么沙箱需要 PVC 卷挂载
AI Agent 的沙箱默认是瞬态的——沙箱进程结束后文件系统随之消失。对于需要长期积累产物的场景(网页快照、报告、模型权重缓存、共享数据集),OpenSandbox 提供了运行时无关的卷模型:在 OSEP-0003: Volume and VolumeBinding Support 中定义了volumes[]字段,每个卷条目包含一个强类型后端结构(host、ossfs、pvc、nfs)以及公共挂载属性(name、mountPath、readOnly、subPath)。
其中pvc后端是运行时无关的抽象:在 Kubernetes 上映射为 PersistentVolumeClaim,在 Docker 上映射为 named volume。也就是说,同一个 SDK 请求可以在不同运行时上表达一致的「挂载持久化命名卷」语义。
volumes: - name: shared-data pvc: claimName: "my-shared-volume" mountPath: /mnt/data从源码看,Python SDK 在 sdks/sandbox/python/src/opensandbox/models/sandboxes.py 中定义了对应的PVC与Volume模型。PVC模型的核心字段如下:
| 字段(SDK) | 字段(API) | 默认值 | 说明 |
|---|---|---|---|
claim_name | claimName | 必填 | Kubernetes 中的 PVC 名称或 Docker named volume 名称 |
create_if_not_exists | createIfNotExists | True | 为true时,卷不存在则自动创建 |
delete_on_sandbox_termination | deleteOnSandboxTermination | False | 为true时,沙箱删除时移除自动创建的卷;预存在的卷永远不会被移除 |
storage_class | storageClass | None | 自动创建 PVC 使用的 StorageClass,None表示集群默认;Docker 运行时忽略 |
storage | storage | None | 自动创建 PVC 的容量请求(如"1Gi");Docker 运行时忽略 |
access_modes | accessModes | None | 自动创建 PVC 的访问模式(如["ReadWriteOnce"]);Docker 运行时忽略 |
而Volume模型通过@model_validator强制校验每个卷条目必须且只能指定一个后端(host/pvc/ossfs),mountPath必须是绝对路径——这些约束在客户端即可提前拦截非法请求。
两种模式的选择:PVC 生命周期归谁所有
OpenSandbox 支持两种 PVC 供应模式,区别在于「谁拥有 PVC 的生命周期」:
| 模式 | createIfNotExists | deleteOnSandboxTermination | PVC 归属 | 适用场景 |
|---|---|---|---|---|
| 自带 PVC(BYO) | false | 忽略 | 你(在集群中预先创建) | 长生命周期共享存储;模型缓存;多个沙箱复用的基线数据集 |
| 服务端托管·持久 | true | false(默认) | 你(首次创建后归你) | 首次使用时按需创建,跨沙箱生命周期保留 |
| 服务端托管·临时 | true | true | 服务端 | 仅限单个沙箱的临时存储;沙箱终止时自动清理 |
两种模式在挂载方式上完全一致,区别只在供应与清理环节。选择的关键问题是:PVC 的生命周期应该由你的平台管理,还是由沙箱自己管理?
前置条件
CSI 驱动
Kubernetes 的 PVC 需要 Container Storage Interface (CSI) 驱动来供应和挂载存储。选择一个与你的存储后端匹配的驱动,例如 Alibaba Cloud CSI Driver 支持:
- Cloud Disk(EBS)——块存储,高性能单节点读写
- NAS——共享文件存储,多节点读写(
ReadWriteMany) - OSS——对象存储,大规模共享读
- CPFS——高性能并行文件系统
- LVM——本地卷管理
OpenSandbox Server
服务端必须运行在 Kubernetes runtime 上,并使用 BatchSandbox 工作负载提供者。官方 Helm chart 已授予 BYO 挂载与服务端自动供应所需的 RBAC 权限(对persistentvolumeclaims的get/create/list/delete/patch)。这一点在服务端源码中有印证:_ensure_pvc_volumes会在挂载前做 PVC 所有权预检,如果服务账号缺少get权限(403),会fail-closed拒绝挂载,而不是降级继续——因为无法确认所有权时,挂载一个已被其他沙箱托管的 PVC 会让当前沙箱暴露在被清理的风险中(见 server/opensandbox_server/services/k8s/kubernetes_service.py)。
Python SDK
uv pip install opensandbox模式一:自带 PVC(Bring Your Own)
适用于 PVC 属于你平台基础设施的一部分的场景——例如多个沙箱复用的共享 NAS,或预置了模型权重的磁盘。
1. 创建 PVC
# pvc.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: my-pvc namespace: opensandbox spec: accessModes: - ReadWriteOnce storageClassName: <your-storage-class> resources: requests: storage: 10Gikubectl apply -f pvc.yaml kubectl get pvc my-pvc -n opensandbox # 应显示 Bound2. 在沙箱中挂载它
from opensandbox import Sandbox from opensandbox.models.sandboxes import PVC, Volume sandbox = await Sandbox.create( image="python:3.11", volumes=[ Volume( name="data-volume", pvc=PVC( claimName="my-pvc", createIfNotExists=False, # 绝不自动供应 BYO claim ), mountPath="/mnt/data", readOnly=False, ), ], ) result = await sandbox.commands.run("ls -la /mnt/data") print("\n".join(msg.text for msg in result.logs.stdout))OpenSandbox 永远不会删除这个 PVC:沙箱终止后 claim 保持 Bound,数据对下一个挂载它的沙箱依然可用。
3. 运行端到端示例
仓库提供了完整的可运行脚本 examples/kubernetes-pvc-volume-mount/main.py,它依次完成:创建沙箱 → 向/mnt/data写入标记文件 → 杀掉沙箱 → 创建第二个绑定同一 PVC 的沙箱 → 确认标记文件仍在。
export OPEN_SANDBOX_API_KEY=your-api-key export OPEN_SANDBOX_BASE_URL=http://localhost:8080 export SANDBOX_PVC_NAME=my-pvc python examples/kubernetes-pvc-volume-mount/main.py脚本中的关键参数均可用环境变量覆盖:SANDBOX_PVC_NAME(默认my-pvc)、SANDBOX_IMAGE(默认python:3.11)。同时脚本为ConnectionConfig设置了request_timeout=timedelta(minutes=10),因为创建带卷的沙箱需要更长等待时间。执行成功后的输出如下:
图中展示了完整的两步验证流程:第一个沙箱写入Hello from OpenSandbox!后被 kill,第二个沙箱挂载同一 PVC 读取到相同内容,最终输出Data persistence verified!。
Pool 模式:预挂载共享 PVC
Pool 的 Pod 在沙箱分配之前就已创建,因此 Pool 共享的存储必须内置于 Pool Pod 模板中。先创建 PVC,再通过Pool.spec.template把它挂载到每个预热 Pod:
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: shared-workspace-pvc namespace: opensandbox spec: accessModes: [ReadWriteMany] storageClassName: <your-rwx-storage-class> resources: requests: storage: 100Gi --- apiVersion: sandbox.opensandbox.io/v1alpha1 kind: Pool metadata: name: shared-workspace-pool namespace: opensandbox spec: template: spec: containers: - name: sandbox-container image: python:3.11 command: ["sleep", "3600"] volumeMounts: - name: shared-workspace mountPath: /workspace volumes: - name: shared-workspace persistentVolumeClaim: claimName: shared-workspace-pvc capacitySpec: bufferMax: 10 bufferMin: 2 poolMax: 20 poolMin: 5先向集群应用 Pool 清单,再从中分配沙箱。沙箱通过extensions.poolRef选择预配置的 Pool,且不再传递每沙箱的volumes列表。
::: warning Pool 静态存储限制 PVC 必须已存在,且其访问模式与存储后端必须允许所有被调度的预热 Pod 挂载它。OpenSandbox 不会创建、变更或删除 Pool 模板引用的 PVC。沙箱创建请求不能在同时使用extensions.poolRef时再添加volumes,因为所选预热 Pod 已存在。 :::
模式二:服务端托管 PVC
适用于沙箱应该自己拥有存储的场景。服务端在 claim 名称第一次被引用时按需创建 PVC,是否在沙箱终止后保留由deleteOnSandboxTermination控制。
按需供应并持久保留(默认)
这是经典的「首个沙箱供应、后续沙箱复用」模式——例如一个崩溃、重启后依然存活的预热缓存,但永远不需要手动kubectl apply。
sandbox = await Sandbox.create( image="python:3.11", volumes=[ Volume( name="cache", pvc=PVC( claimName="agent-cache", createIfNotExists=True, # 首次使用时自动供应 deleteOnSandboxTermination=False, # 默认:保留 PVC storageClass="alibaba-cloud-disk-ssd", # 可选;默认使用集群默认 storage="20Gi", # 可选;默认取服务端配置 accessModes=["ReadWriteOnce"], # 可选;默认 ReadWriteOnce ), mountPath="/mnt/cache", ), ], )第一次调用供应agent-cache;后续使用相同claimName的调用会挂载已有 PVC 并跳过供应。PVC 会一直保留,直到你用kubectl删除它。服务端供应时若未显式指定storage,会回退到服务端配置的storage.volume_default_size(见 kubernetes_service.py 中default_size = self.app_config.storage.volume_default_size的实现)。
按需供应并自动清理
仅限单个沙箱的临时存储——沙箱终止(包括 TTL 到期)时服务端回收 PVC。
sandbox = await Sandbox.create( image="python:3.11", timeout=600, # 10 分钟沙箱 volumes=[ Volume( name="scratch", pvc=PVC( claimName=f"scratch-{run_id}", # 每次运行唯一;opt-in 的 PVC 被独占 createIfNotExists=True, deleteOnSandboxTermination=True, storage="5Gi", ), mountPath="/mnt/scratch", ), ], )::: tip 清理范围 服务端只会删除它按此 opt-in 供应的 PVC。预存在的 PVC 和以deleteOnSandboxTermination=false供应的 PVC 永远不会被触碰。Opt-in 的 PVC 由创建它的沙箱独占——第二个沙箱尝试挂载相同claimName会被以409 CONFLICT拒绝。请为每个沙箱使用唯一的claimName;如果需要共享存储,请使用非 opt-in 或预存在的 PVC。 :::
PVC 生命周期:服务端如何做到精确清理
清理行为由创建时的模式决定:
| 来源 | 沙箱终止时的清理 |
|---|---|
| 预存在 PVC(BYO) | 服务端永不触碰 |
自动创建,deleteOnSandboxTermination=false(默认) | PVC 保留;由调用方负责清理 |
自动创建,deleteOnSandboxTermination=true | 服务端删除 PVC |
对于 opt-in 的 PVC,服务端会打上opensandbox.io/volume-managed-by=server和opensandbox.io/id=<sandbox-id>两个标签,然后通过两条分层路径执行清理(源码实现见 kubernetes_service.py):
ownerReferences(主路径)——PVC 创建后立即被 patch 指向沙箱的工作负载自定义资源。一旦该 CR 被删除(包括由控制器驱动的 TTL 到期,这种路径根本不会到达DELETE /sandboxes/{id}API),Kubernetes 垃圾回收就会级联删除 PVC。- 标签选择器清扫(兜底)——在
DELETE /sandboxes/{id}时,服务端按上述标签列出 PVC 并尽力删除。这能覆盖ownerReferencespatch 失败(例如 RBAC 问题)的情况,确保 API 返回时 PVC 已消失。
两条路径都只匹配服务端打标的 PVC,因此 BYO 和 opt-out 的 claim 永远不会被回收。PVC 删除后,底层 PV 遵循其StorageClass.reclaimPolicy决定去留。
值得一提的是,服务端在创建沙箱前会做所有权预检:即使createIfNotExists=false的请求指向了一个已被其他沙箱打标托管的 PVC,也会被_reject_pvc_owned_by_other_sandbox拦截,避免该沙箱的存储被原 owner 的清理逻辑误删。
重要注意事项
::: warning
- 每沙箱的
volumes不能与extensions.poolRef组合使用。Pool 模式下请按上文所述在Pool.spec.template中预挂载已有共享 PVC。 - 多个沙箱可以挂载同一 PVC(如果访问模式允许,如
ReadWriteMany)——但仅当该 PVC未opt-in 自动清理时。以deleteOnSandboxTermination=true创建的 PVC 由创建沙箱独占,其他沙箱挂载会被服务端以409 CONFLICT拒绝。 - 同一请求中对相同
claimName的所有挂载,createIfNotExists和deleteOnSandboxTermination必须一致;不一致会被以400 INVALID_PARAMETER拒绝。服务端在_ensure_pvc_volumes中按 claim 聚合校验这些标志,一旦发现冲突,在任何副作用发生前直接返回 400(见 kubernetes_service.py)。 :::
继续深入
- 卷模型的完整设计(含
host、ossfs、nfs等后端、跨运行时映射与安全约束):oseps/0003-volume-and-volumebinding-support.md - 可运行的示例脚本:examples/kubernetes-pvc-volume-mount/main.py
- Python SDK 卷模型源码:sdks/sandbox/python/src/opensandbox/models/sandboxes.py
- 服务端 Kubernetes PVC 供应与清理实现:server/opensandbox_server/services/k8s/kubernetes_service.py
- 服务端测试覆盖:PVC 挂载、生命周期与冲突拒绝逻辑见 server/tests/test_routes_snapshots.py 同目录的 k8s 相关测试
- 更完整的示例说明文档:docs/examples/kubernetes-pvc-volume-mount.md
【免费下载链接】OpenSandboxSecure, Fast, and Extensible Sandbox runtime for AI agents.项目地址: https://gitcode.com/GitHub_Trending/ope/OpenSandbox
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考