@cloudflare/sandbox 中的 S3Mount 类会用 s3fs 将 bucket 的一个前缀挂载为目录。你的 Worker 保管 R2 凭据并为每个存储请求签名。本例中,命令从挂载读取输入文件,并在旁边写入摘要。
- 带有 Durable Object 的 Worker,该 Durable Object 使用 Durable Object 调度策略 启动 container。要创建一个,请参阅 运行 Linux 命令。
- 一个 R2 bucket,以及仅对该 bucket 具有 Object Read & Write 权限的 R2 API token。示例使用名为
sandbox-artifacts的 bucket。
当您运行 wrangler deploy 时,必须在本地运行 Docker。对于大多数人来说,安装 Docker 的最佳方式是按照安装 Docker Desktop 的文档 ↗进行操作。其他工具(如 Colima ↗)也可能有效。
您可以通过在终端中运行 docker info 命令来检查 Docker 是否正常运行。如果 Docker 正在运行,该命令将成功执行。如果 Docker 未运行,docker info 命令将挂起或返回包含 "Cannot connect to the Docker daemon" 消息的错误。
-
安装软件包:
npm i @cloudflare/sandboxyarn add @cloudflare/sandboxpnpm add @cloudflare/sandboxbun add @cloudflare/sandbox -
在项目根目录创建包含 FUSE、
s3fs以及S3Mount使用的sandbox-shim二进制文件的Dockerfile:Dockerfiledockerfile FROM node:24-trixie-slim RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates fuse3 s3fs \ && rm -rf /var/lib/apt/lists/* COPY --from=docker.io/cloudflare/sandbox:1.0.1 /usr/local/bin/sandbox-shim /usr/local/bin/sandbox-shim CMD ["sleep", "infinity"]使用与已安装
@cloudflare/sandbox版本匹配的cloudflare/sandbox标签。 -
在
wrangler.jsonc中开启nodejs_compat,将 bucket 的端点和名称添加为变量,将凭据声明为 secrets,并将Dockerfile构建为具名镜像。将<ACCOUNT_ID>替换为你的 Cloudflare 账户 ID:{ "compatibility_flags": ["nodejs_compat"], "vars": { "S3_ENDPOINT": "https://<ACCOUNT_ID>.r2.cloudflarestorage.com", "S3_BUCKET": "sandbox-artifacts", }, "secrets": { "required": ["S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY"], }, "containers": [ { "class_name": "MyContainer", "scheduling_policy": "durable_object", "images": { "artifacts": { "dockerfile": "./Dockerfile", }, }, }, ], }compatibility_flags = [ "nodejs_compat" ] [vars] S3_ENDPOINT = "https://<ACCOUNT_ID>.r2.cloudflarestorage.com" S3_BUCKET = "sandbox-artifacts" [secrets] required = [ "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY" ] [[containers]] class_name = "MyContainer" scheduling_policy = "durable_object" [containers.images.artifacts] dockerfile = "./Dockerfile"secrets.required会将 secrets 添加到生成的类型中,并在它们缺失时让wrangler deploy失败。要挂载来自其他 S3 兼容服务的 bucket,请更改S3_ENDPOINT和S3_BUCKET,并使用该服务的凭据。 -
生成类型,然后将 R2 API token 中的 Access Key ID 和 Secret Access Key 添加为 secrets:
npx wrangler typesyarn wrangler typespnpm wrangler typesnpx wrangler secret put S3_ACCESS_KEY_IDyarn wrangler secret put S3_ACCESS_KEY_IDpnpm wrangler secret put S3_ACCESS_KEY_IDnpx wrangler secret put S3_SECRET_ACCESS_KEYyarn wrangler secret put S3_SECRET_ACCESS_KEYpnpm wrangler secret put S3_SECRET_ACCESS_KEY -
导入
S3Mount,并从 Worker 的主模块导出S3Gateway:src/index.jsjs import { S3Mount } from "@cloudflare/sandbox"; // S3Mount sends storage requests from the sandbox to this entrypoint. export { S3Gateway } from "@cloudflare/sandbox";src/index.tsts import { S3Mount } from "@cloudflare/sandbox"; // S3Mount sends storage requests from the sandbox to this entrypoint. export { S3Gateway } from "@cloudflare/sandbox";sandbox 中的
s3fs会将每个存储请求发送到 container 拦截的地址。拦截会将请求交付给 Worker 中的S3Gateway。S3Gateway会检查请求是否留在挂载的 bucket、前缀和访问模式内。然后它用你的凭据签名请求并将其发送到 R2。 -
向 Durable Object 添加构造函数。它创建一个
S3Mount对象,并在重启后的 Durable Object 发现 container 仍在运行时再次设置不活动超时:src/index.tsts const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; export class MyContainer extends DurableObject<Env> { private readonly container: Container; private readonly mounts: S3Mount; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); const container = ctx.container; if (!container) { throw new Error("No container is configured"); } this.container = container; this.mounts = new S3Mount(container, ctx.exports.S3Gateway); if (container.running) { void ctx.blockConcurrencyWhile(() => container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS), ); } } }只要 Durable Object 在运行,
this.ctx.container就会保持为同一个对象,因此一个S3Mount对象会服务于 Durable Object 启动的每个 container。然后添加启动 sandbox 并为任务挂载前缀的方法:
src/index.tsts export class MyContainer extends DurableObject<Env> { // ... private async mountArtifacts(job: string) { if (!this.container.running) { this.container.start({ image: this.container.images.artifacts, enableInternet: false, }); await this.container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); } await this.mounts.mount({ mountPath: "/mnt/artifacts", source: { type: "s3", endpoint: this.env.S3_ENDPOINT, region: "auto", bucket: this.env.S3_BUCKET, credentials: { type: "static", accessKeyId: this.env.S3_ACCESS_KEY_ID, secretAccessKey: this.env.S3_SECRET_ACCESS_KEY, }, }, keyPrefix: `jobs/${job}`, access: "read-write", }); } }sandbox 中的任何进程都可以读取、更改或删除该前缀下的每个对象。请挂载任务需要的最窄前缀,并在任务只读取时使用
access: "read-only"。sandbox 会收到占位凭据,因为s3fs需要密钥对。挂载可在enableInternet: false下工作。对于短期凭据,请参阅 Credentials。使用相同设置再次调用
mount()会复用现有挂载。每个新挂载都会添加一个出站拦截,unmount()不会移除它。实例支持有限数量的拦截。请为每个任务使用独立的 sandbox 名称,而不是在同一实例中挂载新前缀。 -
添加在挂载目录中运行命令的方法:
src/index.tsts export class MyContainer extends DurableObject<Env> { // ... async digest(job: string) { await this.mountArtifacts(job); const process = await this.container.exec( // The command reads `input.txt` from R2 through the mount // and writes `input.sha256` next to it ["sh", "-c", "sha256sum input.txt > input.sha256"], { cwd: "/mnt/artifacts" }, ); const output = await process.output(); return { exitCode: output.exitCode, stderr: new TextDecoder().decode(output.stderr), }; } }挂载会将文件映射到对象。重命名、锁和权限的行为与本地磁盘不同。将其他系统会读取的结果写入挂载。将工作文件(例如依赖和构建输出)保留在 sandbox 磁盘上。更多信息请参阅 挂载文件行为。
-
添加卸载前缀并停止 sandbox 的方法:
src/index.tsts async finish() { if (!this.container.running) { return; } await this.mounts.unmount("/mnt/artifacts"); await this.container.destroy(); }只有在所有写入挂载目录的作业都结束后,才能调用
finish()。unmount()会在卸载前拒绝存储请求,因此仍在写入的进程会丢失这些写入。如果进程仍在使用目录,unmount()会抛出代码为S3_MOUNT_BUSY的SandboxS3MountError,并继续拒绝请求。停止进程后,再次调用unmount()。 -
向 Worker 添加运行任务并完成它的路由:
src/index.jsjs export default { async fetch(request, env) { const url = new URL(request.url); const match = /^\/jobs\/([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(\/digest)?$/.exec( url.pathname, ); if (!match) { return new Response("Not found", { status: 404 }); } const [, job, digest] = match; const sandbox = env.MY_CONTAINER.getByName(job); if (digest && request.method === "POST") { return Response.json(await sandbox.digest(job)); } if (!digest && request.method === "DELETE") { await sandbox.finish(); return new Response(null, { status: 204 }); } return new Response("Method not allowed", { status: 405 }); }, };src/index.tsts export default { async fetch(request: Request, env: Env): Promise<Response> { const url = new URL(request.url); const match = /^\/jobs\/([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(\/digest)?$/.exec(url.pathname); if (!match) { return new Response("Not found", { status: 404 }); } const [, job, digest] = match; const sandbox = env.MY_CONTAINER.getByName(job); if (digest && request.method === "POST") { return Response.json(await sandbox.digest(job)); } if (!digest && request.method === "DELETE") { await sandbox.finish(); return new Response(null, { status: 204 }); } return new Response("Method not allowed", { status: 405 }); }, } satisfies ExportedHandler<Env>;请先认证调用方,以免其他人运行任务或读取其结果。更多信息请参阅 Sandbox 安全。
-
部署你的 Worker:
npx wrangler deployyarn wrangler deploypnpm wrangler deploy -
为名为
ada的任务上传输入文件,运行任务,并从 R2 读取结果。将示例 hostname 替换为 Wrangler 打印的workers.devURL:echo "artifact input" > input.txt npx wrangler r2 object put sandbox-artifacts/jobs/ada/input.txt --file input.txt --remote curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/jobs/ada/digest --request POST npx wrangler r2 object get sandbox-artifacts/jobs/ada/input.sha256 --remote --pipe任务会以
{"exitCode":0,"stderr":""}响应,R2 中会有摘要:<SHA256_DIGEST> input.txt -
卸载前缀并停止 sandbox:
curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/jobs/ada --request DELETE
在 wrangler dev 下,container 在你机器上的 Docker 中运行,S3Gateway 在本地 Worker 中运行。请将挂载指向你机器上的 S3 兼容 server,而不是 R2。本例中的 server 是 MinIO ↗。FUSE 需要虚拟机中的 Docker(例如 Docker Desktop),或带有 /dev/fuse 的 rootless Linux Docker。更多信息请参阅 FUSE 支持。
-
启动 MinIO 并创建 bucket:
docker run --detach --name minio --publish 9000:9000 minio/minio server /data docker exec minio sh -c "mc alias set local http://localhost:9000 minioadmin minioadmin && mc mb local/sandbox-artifacts" -
在项目根目录创建
.dev.vars文件。其值会覆盖wrangler dev下的变量和 secrets:.dev.varstxt S3_ENDPOINT=http://localhost:9000 S3_ACCESS_KEY_ID=minioadmin S3_SECRET_ACCESS_KEY=minioadmin -
启动开发 server:
npx wrangler devyarn wrangler devpnpm wrangler dev -
上传输入文件,运行任务,并从 MinIO 读取结果:
echo "artifact input" | docker exec --interactive minio mc pipe local/sandbox-artifacts/jobs/ada/input.txt curl http://localhost:8787/jobs/ada/digest --request POST docker exec minio mc cat local/sandbox-artifacts/jobs/ada/input.sha256
- S3Mount API:每个选项和错误。
- 产物工作区示例 ↗:可部署的 Worker,为每个 sandbox 挂载一个前缀并处理其中的文件。
- 使用 snapshot 保存并恢复 sandbox:在实例之间将文件保留在 sandbox 磁盘上。snapshot 不包含挂载的目录。
- S3 API 兼容性:R2 支持的 S3 操作。