基于 Serverless Framework 与 serverless-lift 的 AWS S3 文件自动复制(aws-node-s3-file-replicator)实战指南
基于 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 可直接使用 ESMimport语法;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 的操作指引,验证方式极其简单:
- 登录 AWS S3 控制台,找到输入桶(名称形如
replicator-dev-inputbucket-...,lift 会自动生成带后缀的唯一桶名); - 向输入桶上传任意一个文件;
- 稍候片刻,打开输出桶,即可看到同名对象已经被"即时"复制到位。
也可以在终端验证,例如:
# 上传文件到输入桶
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 入口或人工干预。
六、工程要点与可扩展方向
结合源码结构,可以梳理出几个对该模式后续演进出有价值的要点:
- 权限由框架托管:使用 lift 构造 +
${construct:outputBucket.bucketName}引用后,Serverless Framework 会自动为函数配置对输出桶的s3:CopyObject权限、以及对输入桶读取源对象的权限,无需手写 IAM 策略(对比 Go 版本需在 serverless.yml 中显式声明iam.role.statements)。 - 目标路径可定制:修改 handler.js 中的
destKey,即可实现"归档到子目录""按日期分桶"等更复杂的复制策略。 - 失败可追溯:
replicate对每条记录输出Attempting/Success/Error三级日志,配合 CloudWatch 可以清晰追踪任意一次复制的成败。 - 事件默认语义:未显式指定
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 事件源负责把"文件上传"自动转成"函数调用"。无论是作为跨桶备份、数据迁移还是下游处理的前置管道,这个模板都给出了清晰、可复制、可扩展的参考实现。
进一步参考:
- 服务配置:serverless.yml
- 函数实现:handler.js
- 依赖声明:package.json
- 使用说明:README.md
- 同主题 Go 实现:aws-golang-s3-file-replicator