基于 Serverless Framework 与 serverless-lift 的 AWS S3 文件自动复制(aws-node-s3-file-replicator)实战指南

原创2026-10-13 18:25:481,614 阅读
文章标签:示例工程

基于 Serverless Framework 与 serverless-lift 的 AWS S3 文件自动复制(aws-node-s3-file-replicator)实战指南

本指南以开源仓库 examples 中的 aws-node-s3-file-replicator 示例为蓝本,完整讲解如何用 Node.js + Serverless Framework v4 搭建一个"双桶 S3 文件复制器":当文件上传到 input 桶时,Lambda 函数会被自动触发并把对象复制到 output 桶。读完本文,你将掌握 serverless-lift storage 构造的声明式用法、S3 事件源与 Lambda 的接线方式,以及基于 AWS SDK v3 CopyObjectCommand 的对象复制实现细节。

一、示例要解决什么问题

在很多真实业务里,我们需要把"上传桶"与"归档桶/处理桶"分离:用户文件先写入一个桶,再由后台任务复制或迁移到另一个桶,以隔离权限、控制成本或驱动下游数据处理。本示例用最简洁的方式实现了这一模式——创建两个 S3 桶(inputBucket 与 outputBucket),任何上传到输入桶的文件都会被"即时"复制到输出桶。

  • 语言与运行时:Node.js(ESM 模块),Lambda 运行时 nodejs24.x;
  • 部署框架:Serverless Framework v4(frameworkVersion: '4');
  • 基础设施管理:借助官方插件 serverless-lift 的 storage 构造,以"每个构造对应一个 S3 桶"的方式一键创建两个桶,并使用内置的合理默认值(如桶名自动生成、生命周期规则等)。

整个服务只有一个名为 replicate 的 Lambda 函数,没有 HTTP 端点:它完全由 S3 上传事件驱动,属于典型的事件驱动型无服务器应用。

二、目录结构与关键文件

该示例的完整文件布局如下:

aws-node-s3-file-replicator/
├── handler.js        # Lambda 函数实现(对象复制逻辑)
├── package.json      # 依赖声明(@aws-sdk/client-s3、serverless-lift)
├── serverless.yml    # 服务与资源声明(双桶 + S3 事件源)
└── README.md         # 使用说明

三份核心文件的职责分别是:

文件 作用
serverless.yml 声明服务名、双桶构造、Lambda 函数与 S3 事件触发器
handler.js 接收 S3 事件,对每条记录执行 S3 对象复制
package.json 声明 @aws-sdk/client-s3 运行时依赖与 serverless-lift 开发依赖,并启用 ESM("type": "module")

三、serverless.yml 配置深度解析

serverless.yml 是理解本示例的关键,全文如下:

service: replicator

frameworkVersion: '4'

plugins:
  - serverless-lift

constructs:
  inputBucket:
    type: storage
  outputBucket:
    type: storage

provider:
  name: aws
  runtime: nodejs24.x
  architecture: arm64
  stage: dev
  region: us-east-1

functions:
  replicate:
    handler: handler.replicate
    environment:
      OUTPUT_BUCKET: ${construct:outputBucket.bucketName}
    events:
      - s3:
          bucket: ${construct:inputBucket.bucketName}
          existing: true

逐项拆解如下:

1. 服务与框架版本

  • service: replicator:服务名。部署后 CloudFormation 栈名为 replicator-<stage>(如 replicator-dev),部署输出中也能看到 Deploying "replicator" to stage "dev";
  • frameworkVersion: '4':锁定 Serverless Framework v4,避免大版本升级带来的行为差异。

2. serverless-lift 插件与 constructs 构造

plugins 中声明了 serverless-lift,随后在 constructs 段用 lift 构造语法声明了两个存储桶:

constructs:
  inputBucket:
    type: storage
  outputBucket:
    type: storage

type: storage 即 lift 的 Storage 构造,它会按 lift 的"合理默认值"(sensible defaults)为每个构造各创建一个 S3 桶,包括自动生成的桶名、默认的加密与版本控制等配置。相比手写 resources 段中的裸 S3 资源,lift 构造把"建桶 + 暴露属性 + 关联权限"封装成了一条声明,代码量大幅缩减,这是本示例在基础设施层最大的亮点。

3. 运行时与部署目标

provider:
  name: aws
  runtime: nodejs24.x
  architecture: arm64
  stage: dev
  region: us-east-1
  • runtime: nodejs24.x:当前 Node.js 的受支持 Lambda 运行时之一,配合 package.json 中的 "type": "module",handler.js 可直接使用 ESM import 语法;
  • architecture: arm64:使用 AWS Graviton2 处理器架构,通常可带来更优的性价比(AWS Lambda 支持 arm64 与 x86_64 两种架构,arm64 为当前常见选择);
  • stage: dev、region: us-east-1:默认部署阶段与区域,均可通过 --stage / --region 覆盖。

4. 函数定义与 S3 事件源

functions:
  replicate:
    handler: handler.replicate
    environment:
      OUTPUT_BUCKET: ${construct:outputBucket.bucketName}
    events:
      - s3:
          bucket: ${construct:inputBucket.bucketName}
          existing: true
  • handler: handler.replicate:入口指向 handler.js 中导出的 replicate 函数;
  • environment.OUTPUT_BUCKET:通过 ${construct:outputBucket.bucketName} 把 lift 构造生成的输出桶名注入 Lambda 环境变量,函数在运行时从 process.env.OUTPUT_BUCKET 读取目标桶;
  • events 中的 s3 事件:监听 ${construct:inputBucket.bucketName} 指向的输入桶,existing: true 表示该桶已经由 lift 构造创建,Serverless Framework 不再重复创建桶,而是直接为既有桶配置 S3 事件通知;
  • 该写法省略了 event 字段,采用 Serverless Framework 对 S3 事件源的默认行为:默认监听 s3:ObjectCreated:*(对象创建类事件),即覆盖上传(PUT/POST)、复制(Copy)与 multipart 完成等所有"创建"场景。

四、Lambda 复制逻辑源码解析

核心实现位于 handler.js,完整代码如下:

import { S3Client, CopyObjectCommand } from '@aws-sdk/client-s3';

const s3 = new S3Client({});
const outputBucket = process.env.OUTPUT_BUCKET;

export const replicate = async (event) => {
  // Fail on missing data
  if (!outputBucket) {
    throw new Error('Error: Environment variable OUTPUT_BUCKET missing');
  }
  if (event.Records === null) {
    throw new Error('Error: Event has no records.');
  }

  await Promise.all(event.Records.map((record) => replicateObject(record, outputBucket)));
};

async function replicateObject(record, destBucket) {
  // The source bucket and source key are part of the event data
  const srcBucket = record.s3.bucket.name;
  const srcKey = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));

  // Modify destKey if an alternate copy location is preferred
  const destKey = srcKey;

  const msg = `copying ${srcBucket}:${srcKey} to ${destBucket}:${destKey}`;

  console.log(`Attempting: ${msg}`);
  try {
    await s3.send(
      new CopyObjectCommand({
        Bucket: destBucket,
        Key: destKey,
        CopySource: encodeURIComponent(`${srcBucket}/${srcKey}`),
        MetadataDirective: 'COPY',
      })
    );
    console.log(`Success: ${msg}`);
  } catch (err) {
    console.log(`Error: ${msg}`, err);
    throw err;
  }
}

1. 环境变量与输入校验

函数先读取 process.env.OUTPUT_BUCKET(由 serverless.yml 注入),并做两道防御性检查:目标桶环境变量缺失、或事件中没有 Records 时直接抛错,避免在无效输入上继续执行。

2. 源桶与源键的提取

S3 事件中,record.s3.bucket.name 给出源桶名,record.s3.object.key 给出对象键。这里有一个值得注意的细节:

const srcKey = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));

S3 事件通知中的对象键是经过 URL 编码的(例如文件名中的空格会表现为 + 或 %20),因此先 replace(/\+/g, ' ') 还原空格,再用 decodeURIComponent 完成整体解码,保证含空格、中文等特殊字符的文件名也能被正确复制。这也是复制类 Lambda 处理 S3 事件时最常见的坑之一。

3. 对象复制(CopyObject)

复制通过 AWS SDK v3 的 CopyObjectCommand 完成:

await s3.send(
  new CopyObjectCommand({
    Bucket: destBucket,
    Key: destKey,
    CopySource: encodeURIComponent(`${srcBucket}/${srcKey}`),
    MetadataDirective: 'COPY',
  })
);
  • CopySource 必须为 bucket/key 形式的字符串,并再次 encodeURIComponent 编码,与上面 S3 事件键的 URL 编码规则相呼应;
  • MetadataDirective: 'COPY' 表示复制时保留源对象的元数据(而不重新设置);
  • 目标键默认与源键保持一致(const destKey = srcKey;),源码注释明确提示:如果需要复制到其他路径,修改 destKey 即可,例如加前缀 archive/ 或按日期分目录。

4. 批处理与错误传播

await Promise.all(event.Records.map((record) => replicateObject(record, outputBucket)));

S3 事件可能一次携带多条记录(如批量上传时聚合触发),函数用 Promise.all 并行复制全部对象;单条复制失败时捕获错误并打印日志后重新抛出,让 Lambda 返回失败、触发 S3 事件的 retry 机制,保证不静默丢失对象。

补充:仓库中还提供了同主题的 Go 版本 aws-golang-s3-file-replicator,其 src/main.go 使用 CopyObjectInput 实现相同复制逻辑,并在 serverless.yml 中通过 custom 变量 + ${env:...} 默认值组合配置双桶,可作为多语言实现对照参考。

五、安装与部署

1. 安装依赖

npm install

该命令会安装 package.json 中的两类依赖:

  • 运行时依赖 @aws-sdk/client-s3(版本要求 ^3.700.0),用于 S3 复制调用;
  • 开发依赖 serverless-lift(^1.1.0),用于在部署时解析 constructs 并创建双桶。

2. 部署服务

serverless deploy

部署成功后,控制台会输出与 README 一致的典型结果:

Deploying "replicator" to stage "dev" (us-east-1)

✔ Service deployed to stack replicator-dev (65s)

functions:
  replicate: replicator-dev-replicate (1.1 kB)

从输出可以看到:CloudFormation 栈 replicator-dev 创建完成,replicate 函数已就绪(打包体积仅约 1.1 kB)。期间 serverless-lift 会在栈中创建 inputBucket 与 outputBucket 两个 S3 桶,并完成输入桶的 S3 事件通知配置(此处使用 existing: true,即复用 lift 已建好的桶)。

3. 验证复制效果

按 README 的操作指引,验证方式极其简单:

  1. 登录 AWS S3 控制台,找到输入桶(名称形如 replicator-dev-inputbucket-...,lift 会自动生成带后缀的唯一桶名);
  2. 向输入桶上传任意一个文件;
  3. 稍候片刻,打开输出桶,即可看到同名对象已经被"即时"复制到位。

也可以在终端验证,例如:

# 上传文件到输入桶
aws s3 cp ./test.txt s3://<input-bucket-name>/test.txt

# 检查输出桶中是否出现该对象
aws s3 ls s3://<output-bucket-name>/

整个链路为:对象创建事件 → S3 通知 → 触发 replicate Lambda → 读取 OUTPUT_BUCKET 环境变量 → CopyObjectCommand 复制到输出桶,全程无需任何 HTTP 入口或人工干预。

六、工程要点与可扩展方向

结合源码结构,可以梳理出几个对该模式后续演进出有价值的要点:

  1. 权限由框架托管:使用 lift 构造 + ${construct:outputBucket.bucketName} 引用后,Serverless Framework 会自动为函数配置对输出桶的 s3:CopyObject 权限、以及对输入桶读取源对象的权限,无需手写 IAM 策略(对比 Go 版本需在 serverless.yml 中显式声明 iam.role.statements)。
  2. 目标路径可定制:修改 handler.js 中的 destKey,即可实现"归档到子目录""按日期分桶"等更复杂的复制策略。
  3. 失败可追溯:replicate 对每条记录输出 Attempting/Success/Error 三级日志,配合 CloudWatch 可以清晰追踪任意一次复制的成败。
  4. 事件默认语义:未显式指定 event 时,S3 事件源默认订阅 s3:ObjectCreated:*;若需要额外处理删除事件或指定前缀过滤,可在 serverless.yml 的 events.s3 中补充 event 与 rules 字段。

七、小结

aws-node-s3-file-replicator 用极简的双文件结构(一个 serverless.yml、一个 handler.js),演示了 Serverless Framework v4 生态中一套完整且可直接套用的 S3 事件驱动复制链路:serverless-lift 的 storage 构造负责声明式建桶与权限托管,Lambda 函数通过 AWS SDK v3 的 CopyObjectCommand 完成对象复制,S3 事件源负责把"文件上传"自动转成"函数调用"。无论是作为跨桶备份、数据迁移还是下游处理的前置管道,这个模板都给出了清晰、可复制、可扩展的参考实现。

进一步参考:

登录后查看全文
examples