CubeSandbox S3 兼容 Volume 插件:为 AI Agent 沙箱接入 AWS S3 / MinIO / COS / R2 生命周期持久卷
【免费下载链接】CubeSandboxInstant, Concurrent, Secure & Lightweight Sandbox for AI Agents.项目地址: https://gitcode.com/GitHub_Trending/cu/CubeSandbox
本篇是 CubeSandbox 开源仓库中 S3 兼容 Volume 插件的完整操作手册与实现解读。它面向需要手工部署插件、或将默认捆绑的 MinIO 替换为任意 S3 兼容对象存储的运维与开发人员,完整覆盖依赖安装、插件构建、CubeMaster/Cubelet 双侧注册、SDK 验证与故障排查。读完本文,你将能够在自己的 Cube 集群上把 AWS S3、腾讯云 COS、Cloudflare R2、MinIO 等对象存储变成 AI Agent 沙箱的持久卷,实现“销毁沙箱、数据保留、下次重挂”的生命周期持久化。
插件定位:给临时沙箱一块“永生”的磁盘
CubeSandbox 的沙箱默认是临时的——沙箱被销毁后,其内部数据随之消失。S3 兼容 Volume 插件为沙箱提供用户作用域(user-scoped)的持久卷:通过 e2b 兼容的/volumesAPI 完成卷的创建、挂载、卸载与删除,底层由对象存储承载数据。插件本身是单个静态 Go 二进制,内置 S3 客户端(控制面不再需要任何 S3 命令行工具),数据面挂载则使用标准的s3fsFUSE 驱动。整个方案与厂商无关——后端不过是配置文件里的一个ENDPOINT字段。
该插件以 COS 插件为蓝本,但有两处关键差异(见 examples/volume/s3/README.md):
- 后端从腾讯云 COS 泛化为任意 S3 兼容端点;
- 挂载驱动 s3fs 同时支持
amd64与arm64(cosfs 仅支持amd64)。
两者可以在同一集群中共存(默认安装会同时注册cos与s3两个 driver)。
版本要求:Cube 平台 ≥ 0.6.0,Python SDK
cubesandbox≥ 0.6.0。Hook 协议与框架细节见 Volume Plugin framework。
前置条件与“你是否需要本页”
在动手之前,先对照下表确认自己的处境:
| 你的情况 | 去向 |
|---|---|
| 默认安装(一键安装 / Helm 默认),只是想用 S3 Volume | 直接看用户教程 S3 Volumes——安装器已启动 MinIO、装好插件、写好凭据,直接用 SDK 即可,无需任何配置 |
| 想把捆绑的 MinIO 换成外部 S3 | 从下文「构建并安装插件与凭据」一节开始,只需编辑volume-s3.conf |
| 从零部署 S3 Volume 插件 | 阅读本文全部内容 |
前置条件清单:
| 项目 | 说明 |
|---|---|
| 运行中的 Cube 集群 | 至少包含CubeMaster、Cubelet、CubeAPI(端口通常为3000) |
| 沙箱模板 | 一个templateID(见下文 SDK 验证一节) |
| S3 兼容存储 | 一个 bucket,以及一对对该 bucket 具备读写权限的访问密钥(bucket 缺失时插件会自动创建) |
| 本机权限 | CubeMaster / Cubelet 宿主机上的sudo(用于安装软件、编辑配置、重启服务) |
单机开发:CubeMaster 与 Cubelet 在同一主机上,依赖只需安装一次。多节点:按下一节的表格在对应节点分别安装。
1. 安装依赖:谁需要什么
插件控制面(Create/Destroy)通过 HTTP 直接访问端点,因此仅充当 CubeMaster 的节点本小节什么都不用装;数据面(Attach/Detach)才需要挂载工具:
| 工具 | 安装位置 | 用途(Hook) |
|---|---|---|
| s3fs | Cubelet | attach / detach(FUSE 挂载) |
| jq | 任意节点(可选) | 调试时手工阅读插件输出 |
方式 A:安装脚本
Cubelet 节点:
sudo ./install-deps.sh --s3fs单机(两种角色同宿主机),并附带 jq 便于调试:
sudo ./install-deps.sh --all只检查不安装,追加--check-only。脚本同时兼容 Debian/Ubuntu(apt,安装s3fs)与 RHEL 系(dnf/yum,安装s3fs-fuse,依赖 EPEL),见 examples/volume/s3/install-deps.sh。
方式 B:手动安装
# Cubelet — Debian/Ubuntu sudo apt-get install -y s3fs # Cubelet — RHEL/CentOS(需要 EPEL) sudo yum install -y epel-release && sudo yum install -y s3fs-fuse验证安装
Cubelet —— s3fs
ls /dev/fuse && echo "FUSE ok" s3fs --version | head -1两者都必须成功;/dev/fuse缺失会直接导致 attach 失败。
CubeMaster —— 用插件的 create 钩子验证凭据
插件自身的create钩子就是最好的校验:凭据错误、端点错误或权限缺失都会立刻报错。
/usr/local/services/cubetoolbox/CubeMaster/plugin/cube-volume-s3 \ --op create --volume-id preflight-check --name preflight /usr/local/services/cubetoolbox/CubeMaster/plugin/cube-volume-s3 \ --op destroy --volume-id preflight-check两条命令都必须输出"error":""且退出码为 0。出现InvalidAccessKeyId或AccessDenied说明密钥对缺少对 bucket 的读写权限。
2. 构建并安装插件与凭据
构建二进制
从仓库根目录构建(使用项目构建器镜像,无需本地 Go 工具链):
make cube-volume-s3 # -> _output/bin/cube-volume-s3或使用本地 Go 工具链(≥ 1.25):
cd examples/volume/s3 && make # -> bin/cube-volume-s3一键发布包与容器镜像已内置编译好的二进制,位于<prefix>/{CubeMaster,Cubelet}/plugin/cube-volume-s3。
安装到两个 plugin 目录
PREFIX=/usr/local/services/cubetoolbox sudo install -m 0755 _output/bin/cube-volume-s3 \ "$PREFIX/CubeMaster/plugin/cube-volume-s3" sudo install -m 0755 _output/bin/cube-volume-s3 \ "$PREFIX/Cubelet/plugin/cube-volume-s3" sudo install -m 0600 volume-s3.conf.example \ "$PREFIX/CubeMaster/plugin/volume-s3.conf" sudo install -m 0600 volume-s3.conf.example \ "$PREFIX/Cubelet/plugin/volume-s3.conf"编辑volume-s3.conf
配置文件采用KEY=VALUE行格式(保持与source兼容),由插件自行解析而不是执行(见 internal/config/config.go 的Load实现)。字段如下:
| 字段 | 说明 | 必填 |
|---|---|---|
ACCESS_KEY_ID | Access key ID | 是 |
SECRET_ACCESS_KEY | Secret access key | 是 |
BUCKET | 承载所有卷的 bucket | 是 |
ENDPOINT | S3 兼容端点 URL(见下表) | 是 |
REGION | SigV4 签名区域;默认us-east-1 | 否 |
S3FS_EXTRA_OPTS | 额外 s3fs 挂载选项,以空白分隔(如 MinIO 需要-ouse_path_request_style)。多选项的值可加引号以保持文件可被source;插件会去除引号。设置-ouse_path_request_style同时会把插件自身 S3 客户端切换为 path-style 寻址 | 否 |
常见后端端点对照:
| 提供商 | ENDPOINT | REGION |
|---|---|---|
| AWS S3 | https://s3.<region>.amazonaws.com | bucket 所在区域 |
| 腾讯云 COS | https://cos.<region>.myqcloud.com | bucket 所在区域(如ap-guangzhou) |
| Cloudflare R2 | https://<account-id>.r2.cloudflarestorage.com | auto |
| MinIO | http://<minio-host>:9000 | 任意值 |
完整模板见 examples/volume/s3/volume-s3.conf.example。配置含明文密钥,必须 root 属主、权限600:
sudo chown root:root "$PREFIX/CubeMaster/plugin/volume-s3.conf" "$PREFIX/Cubelet/plugin/volume-s3.conf" sudo chmod 600 "$PREFIX/CubeMaster/plugin/volume-s3.conf" "$PREFIX/Cubelet/plugin/volume-s3.conf"插件在可执行文件同目录下查找volume-s3.conf,也可用环境变量CUBE_S3_CONFIG覆盖路径(源码中还支持CUBE_S3_PASSWD_FILE、CUBE_S3_LOCK_DIR两个调试/CI 覆盖项,见 cmd/cube-volume-s3/main.go 的包注释)。挂载基目录不在此处配置——由 Cubelet 在 attach 时传入(默认/data/cube-shared/volume,见下一节)。
从源码看,配置解析还做了两项防御:internal/config/config.go 会校验四个必填字段非空,并支持
ADDRESSING_STYLE=path旧字段兼容;解析时按第一个#截断行内注释(与 shellsource行为一致),但值内不支持带引号的#。
3. 配置 CubeMaster
编辑 CubeMaster 配置(常见路径/usr/local/services/cubetoolbox/CubeMaster/conf.yaml),添加Controller插件(Create / Destroy):
volume_plugins: - name: s3 type: binary binary_path: /usr/local/services/cubetoolbox/CubeMaster/plugin/cube-volume-s3name: s3就是 API / SDK 眼中的driver。当Volume.create("x")省略 driver 时,使用列表中的第一个条目——默认安装现在把s3排在第一位,因此省略 driver 即路由到 S3。
4. 配置 Cubelet
编辑 Cubelet 配置(常见路径/usr/local/services/cubetoolbox/Cubelet/config/config.toml)。
确认挂载父目录(可选;以下为默认值):
[plugins."io.cubelet.internal.v1.storage"] volume_plugin_base_dir = "/data/cube-shared/volume"添加Node插件(Attach / Detach):
[[plugins."io.cubelet.internal.v1.storage".volume_plugins]] name = "s3" type = "binary" binary_path = "/usr/local/services/cubetoolbox/Cubelet/plugin/cube-volume-s3"name必须与 CubeMaster 一致(本例均为s3)。插件返回的host_path形如<volume_plugin_base_dir>/s3-<volumeID>,满足框架对host_path必须位于volumeBaseDir之内的要求(框架层约束详见 docs/guide/volume-plugin.md 的 Registration and Configuration 一节)。
5. 重启服务并验证
sudo systemctl restart cube-sandbox-cubemaster sudo systemctl restart cube-sandbox-cubelet sudo systemctl restart cube-sandbox-cube-api sleep 5 systemctl is-active cube-sandbox-cubemaster cube-sandbox-cubelet cube-sandbox-cube-api验证插件已加载:
grep -aF '[volume] registered' /data/log/CubeMaster/cubemaster-req.log | tail -5 grep -aF '[plugin_volume] initialized' /data/log/Cubelet/Cubelet-req.log | tail -5预期输出:
[volume] registered binary plugin "s3" at /usr/local/services/cubetoolbox/CubeMaster/plugin/cube-volume-s3 [plugin_volume] initialized binary plugin "s3" at /usr/local/services/cubetoolbox/Cubelet/plugin/cube-volume-s3手动 attach 测试(在 Cubelet 节点上):
/usr/local/services/cubetoolbox/Cubelet/plugin/cube-volume-s3 \ --op attach \ --sandbox-id test-sandbox \ --namespace default \ --volume-id test-vol \ --ref-count 0 \ --volume-base-dir /data/cube-shared/volume成功标志:stdout 输出一行 JSON,包含"host_path":"/data/cube-shared/volume/s3-test-vol"且"error":""。
测试后清理:
/usr/local/services/cubetoolbox/Cubelet/plugin/cube-volume-s3 \ --op detach --sandbox-id test-sandbox --namespace default \ --volume-id test-vol --ref-count 0 \ --metadata '{"mount_dir":"/data/cube-shared/volume/s3-test-vol"}'钩子调用契约(源码视角)
每次操作是一次独立进程:cube-volume-s3 --op <op> [--<key> <value> ...],stdout 输出单个 JSON 对象,日志走 stderr,退出码 0 且"error":""表示成功(见 cmd/cube-volume-s3/main.go)。main.go中的parseFlags定义了--op、--volume-id、--name、--sandbox-id、--namespace、--ref-count、--volume-base-dir、--private-data、--metadata等参数,run函数按--op分发到四个钩子:
create:确保 bucket 存在(缺失时创建),然后 PUT 0 字节的volumes/<id>/目录对象;destroy:递归删除该前缀下的所有对象;attach:执行 s3fs 挂载并返回host_path(幂等——重复 attach 复用已有挂载点,返回的metadata记录mount_dir与volume_id);detach:refCount > 0时直接返回成功(不卸载),refCount == 0时才执行卸载。
6. 准备 SDK 环境
在开发机上(必须能访问 CubeAPI):
pip install 'cubesandbox>=0.6.0' export CUBE_API_URL=http://<cubeapi-host>:3000 export CUBE_TEMPLATE_ID=<your-template-id> # 挂载卷的远程沙箱 I/O 需要(数据面走 CubeProxy) export CUBE_PROXY_NODE_IP=<cubeproxy-or-cubelet-node-ip> # 集群开启鉴权时: # export CUBE_API_KEY=<your-key>7. 用 SDK 验证完整生命周期
from cubesandbox import Sandbox, Volume # ① 创建 Volume(bucket 中会出现目录对象 volumes/<id>/) vol = Volume.create("my-data", driver="s3") print("volume_id:", vol.volume_id) # ② 创建沙箱并挂载 with Sandbox.create(volume_mounts={"/workspace": vol}) as sb: sb.files.write("/workspace/hello.txt", "from S3 volume") print(sb.files.read("/workspace/hello.txt")) # ③ 退出 with → 沙箱销毁、卷卸载(bucket 数据保留) # ④ 删除 Volume(bucket 前缀被清除——不可逆) Volume.destroy(vol.volume_id) print("done")确认对象落进了 bucket。任意 S3 浏览器均可;MinIO 的mc是单二进制、无需 Python:
source /usr/local/services/cubetoolbox/CubeMaster/plugin/volume-s3.conf mc alias set cube "$ENDPOINT" "$ACCESS_KEY_ID" "$SECRET_ACCESS_KEY" mc ls --recursive "cube/$BUCKET/volumes/"确认 Cubelet 挂载命名空间内的 s3fs 挂载(沙箱运行期间执行):
CPID=$(pgrep -f "cubelet --config" | head -1) nsenter -t "$CPID" -m -- cat /proc/mounts | grep s3fs调试提示:Cubelet 运行在独立的挂载命名空间(
unshare(CLONE_NEWNS))中,宿主根命名空间的/proc/mounts通常看不到这些 FUSE 挂载点。binary 插件由 Cubelet fork 出来、天然继承该命名空间,无需 nsenter;手动在宿主上挂载通常不会出现在沙箱内(见 docs/guide/volume-plugin.md Debugging 一节)。
自动化验证
COS 示例的 examples/volume/cos/verify_volume.py 与 driver 无关——把它指向本 driver 即可:
cd ../cos export CUBE_API_URL=http://127.0.0.1:3000 export CUBE_TEMPLATE_ID=tpl-xxxx export CUBE_PROXY_NODE_IP=127.0.0.1 export CUBE_VOLUME_DRIVERS=s3 # 脚本默认跳过 cfs/s3/nfs(这些在 COS 演示环境中未部署); # 本环境已部署 s3,清空跳过列表: export CUBE_VOLUME_SKIP_DRIVERS= python3 verify_volume.py该脚本覆盖了 HTTP 响应契约(POST /volumes201、DELETE204、删除后GET404)、逐 driver 的完整生命周期(创建→列出→单查→进沙箱检查挂载点与读写→删除→确认消失)、多沙箱共享、跨沙箱持久化,以及挂载不存在卷、非法卷名、未知 driver 等负向用例,并输出分组报告。
8. 故障排查
| 症状 | 检查 |
|---|---|
unknown driver: s3 | CubeMastervolume_plugins缺条目,或未重启 |
no plugin registered for driver "s3" | Cubelet 缺同名插件,或未重启 |
Attach 失败,s3fs mount failed | ls /dev/fuse;核对volume-s3.conf中的凭据与ENDPOINT;运行上文第 5 节的手动 attach 查看 s3fs 具体报错 |
Attach 失败,s3fs 日志出现NoSuchKey(针对volumes/<id>/) | Create 必须 PUT 带尾斜杠的目录对象(s3fs mkdir 依赖它)。前缀下放一个.keep文件是另一个 key,不满足该 stat。若旧版本插件仍在写.keep,请升级插件 |
open config ...: no such file or directory | volume-s3.conf必须位于插件二进制同目录,或用CUBE_S3_CONFIG指向它 |
InvalidAccessKeyId/SignatureDoesNotMatch | 密钥对错误、缺少 bucket 权限,或REGION与端点 SigV4 期望不符 |
| bucket 名含点号 | s3fs 默认使用 virtual-hosted-style 寻址,对含点号的 bucket 名会破坏 TLS。改用不含点号的 bucket,或在volume-s3.conf中设置S3FS_EXTRA_OPTS=-ouse_path_request_style(MinIO 通常也需要) |
| SDK 写失败 | CUBE_PROXY_NODE_IP未设置;CubeAPI 或模板未 READY |
省略 driver 的Volume.create没有走 s3 | 默认安装中s3是volume_plugins首项(默认 driver);若未生效,检查volume_plugins的顺序 |
更多平台级排查见 Volume Plugin framework 的 Troubleshooting 章节。
后端布局:一个 bucket、每卷一个前缀
<bucket>/volumes/<volumeID>/ ← 每个 Volume 一个前缀Attach 时,s3fs 将BUCKET:/volumes/<volumeID>挂载到宿主机/data/cube-shared/volume/s3-<volumeID>/,随后 Cubelet 通过 virtiofs 将该目录暴露给 microVM。
Hook 行为与 RefCount
| Hook | 侧 | refCount | 行为 |
|---|---|---|---|
| Create | Controller | — | 确保 bucket 存在(缺失时创建),然后 PUT 0 字节volumes/<id>/对象(s3fs 目录对象) |
| Destroy | Controller | — | 列出并删除前缀下所有对象 |
| Attach | Node | 0 | s3fs 挂载 → 返回host_path |
| Attach | Node | > 0 | 返回已有host_path;不重复挂载 |
| Detach | Node | > 0 | 无操作 |
| Detach | Node | 0 | fusermount -u;保留bucket 中的数据 |
RefCount 语义与框架一致:Cubelet 维护每节点引用计数并传给 Node Hook,节点本地计数翻转(0→1 或 1→0)时 Cubelet 会通知 CubeMaster 更新t_cube_volume.refcount,控制面DELETE /volumes在该计数非零时被拒绝(409),详见 docs/guide/volume-plugin.md 的 RefCount 小节。
源码级实现细节
- 挂载幂等与安全:attach/detach 通过
flock序列化同一节点上同一卷的并发操作(见 internal/lockfile/lockfile.go——插件每次操作都是独立短进程,锁必须是跨进程的文件锁,进程内 Mutex 无法串行化)。s3fs 挂载后还要用mountpoint -q复核“真的是挂载点”,因为 s3fs 认证失败时也可能退出码为 0(见 internal/s3fsmnt/mount.go)。 - s3fs 参数构造:
MountArgs组装-ourl=<ENDPOINT>、-oendpoint=<REGION>、-opasswd_file=、-oallow_other(Cubelet 以不同用户遍历挂载点做 virtiofs 绑定)并追加S3FS_EXTRA_OPTS。凭据文件按 bucket 区分(/etc/cube/.passwd-s3fs-volume-<bucket>),仅当内容变化时才重写,避免多插件实例竞争(见 internal/s3fsmnt/mount.go 的EnsurePasswdFile)。 - 控制面客户端:Create/Destroy 使用 minio-go(
credentials.NewStaticV4),PathStyle与 s3fs 的-ouse_path_request_style联动,保证控制面与数据面寻址方式一致;ParseEndpoint明确拒绝带路径前缀的端点(如https://host/s3proxy),因为 minio-go 无法携带 URL 路径,会导致控制面与数据面分裂到不同根(见 internal/s3api/client.go)。 - 销毁的容错边界:
RemoveVolumeDir只把 bucket 或 key “不存在”视为成功(前缀已不存在),其余错误一律向上传播,避免 CubeMaster 在对象尚未删除时丢弃卷记录;并发首次建桶时用BucketAlreadyOwnedByYou/BucketAlreadyExists容错(见 internal/s3api/client.go)。
设计要点总结
- 一个 bucket、每卷一个前缀:与 COS 示例一致。多 bucket 场景通常运行多个插件实例并取不同
driver名,或扩展 Create 接受 bucket。框架只要求 Hook 协议与driver一致性。 - 无需 S3 命令行工具:控制面在插件二进制内使用 minio-go——一个约 8MB 的静态二进制,替代了每个控制节点上约 100MB 的 AWS CLI 安装。
- bucket 自动创建:Create 先探测 bucket;已存在的 bucket不需要
s3:CreateBucket权限,只有缺失时才会创建(典型场景即捆绑的 MinIO)。 - Destroy 仅容忍 not-found:bucket 或 key 缺失视为前缀已删;其余错误全部传播,防止卷记录被删而对象残留。
- 凭据永不进入沙箱:它们只存在于 CubeMaster/Cubelet 上 root 属主、权限
600的配置中;microVM 看到的只是一个文件系统。 private_data承载 key 前缀:Create 的结果(最大 1024 字节,永不下发 SDK 客户端)在 Attach 时回传,attach 据此记录挂载来源。- 并发安全:同卷 attach/detach 由 per-volume
flock串行化,两个沙箱同时启动不会重复挂载。 - Destroy 不可逆:会删除整个
volumes/<id>/前缀。API 的 refcount 保护(挂载中DELETE /volumes返回 409)是防止删除已挂载卷的手段;真正的风险是节点崩溃后残留过期 refcount——集群级计数可能已归零而节点仍持有挂载。
仓库布局与测试
examples/volume/s3/ ├── Makefile # build / fmt / lint / test ├── install-deps.sh # 宿主依赖 + 检查(s3fs / jq) ├── volume-s3.conf.example ├── cmd/cube-volume-s3/main.go # 参数解析、Hook 分发、stdout JSON └── internal/ ├── config/ # volume-s3.conf 解析 ├── s3api/ # 经 minio-go 实现 create / destroy ├── s3fsmnt/ # s3fs 挂载 / 卸载 └── lockfile/ # 跨进程 per-volume flock运行单元测试(无需云端访问):
cd examples/volume/s3 && make test # 或,在项目构建器镜像中从仓库根目录执行: make cube-volume-s3-testMakefile 使用CGO_ENABLED=0产出静态二进制,保证在任意 glibc/musl 宿主上可运行(见 examples/volume/s3/Makefile)。插件同时覆盖config、s3api、s3fsmnt、lockfile四个包的单元测试(*_test.go),不依赖真实云环境即可验证解析、端点校验、参数构造与锁语义。
延伸阅读
| 文档 | 内容 |
|---|---|
| S3 Volumes(用户教程) | 终端用户快速上手 |
| Volume Plugin framework | 协议、RefCount、Hook 语义、源码索引 |
| COS 示例 | 本插件所参照的参考实现(binary Shell + rpc Go) |
| verify_volume.py | 与 driver 无关的 SDK 集成验证脚本 |
【免费下载链接】CubeSandboxInstant, Concurrent, Secure & Lightweight Sandbox for AI Agents.项目地址: https://gitcode.com/GitHub_Trending/cu/CubeSandbox
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考