跳转到内容
本站为社区维护的非官方中文翻译,与 Cloudflare, Inc. 无关联。本站不提供账户登录,请勿在任何非官方站点输入您的 Cloudflare 账户信息。官方文档:developers.cloudflare.com

挂载 R2 bucket

最后更新 查看 MarkdownAgent 设置

@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" 消息的错误。

挂载 bucket 前缀

  1. 安装软件包:

    npm i @cloudflare/sandbox
  2. 在项目根目录创建包含 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 标签。

  3. 在 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,并使用该服务的凭据。

  4. 生成类型,然后将 R2 API token 中的 Access Key ID 和 Secret Access Key 添加为 secrets:

    npx wrangler types
    npx wrangler secret put S3_ACCESS_KEY_ID
    npx wrangler secret put S3_SECRET_ACCESS_KEY
  5. 导入 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。

  6. 向 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 名称,而不是在同一实例中挂载新前缀。

  7. 添加在挂载目录中运行命令的方法:

    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 磁盘上。更多信息请参阅 挂载文件行为。

  8. 添加卸载前缀并停止 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()。

  9. 向 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 安全。

  10. 部署你的 Worker:

    npx wrangler deploy
  11. 为名为 ada 的任务上传输入文件,运行任务,并从 R2 读取结果。将示例 hostname 替换为 Wrangler 打印的 workers.dev URL:

    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
  12. 卸载前缀并停止 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 支持。

  1. 启动 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"
  2. 在项目根目录创建 .dev.vars 文件。其值会覆盖 wrangler dev 下的变量和 secrets:

    .dev.varstxt
    S3_ENDPOINT=http://localhost:9000
    S3_ACCESS_KEY_ID=minioadmin
    S3_SECRET_ACCESS_KEY=minioadmin
  3. 启动开发 server:

    npx wrangler dev
  4. 上传输入文件,运行任务,并从 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

相关资源

这篇文档对您有帮助吗?