如何为自定义 REST API 实现按使用量计费?
结论
可以。比较合适的做法是采用 Serverless 架构:用 Amazon API Gateway 接收 REST 请求,由 AWS Lambda 处理业务逻辑,再按需接入 DynamoDB、SES 或其他托管服务。
这类服务主要按请求次数、执行时间和资源用量收费,不需要让 EC2 实例长期运行。没有请求时,API Gateway 和 Lambda 通常不会产生计算费用,但日志、数据库、邮件和域名等配套服务仍可能收费。
典型调用链如下:
CloudFront / Browser
|
v
Amazon API Gateway
|
v
AWS Lambda
|
+--> DynamoDB:保存邀请记录
|
+--> Amazon SES:发送邀请邮件
为什么不必常驻 Node.js 或 Sinatra 服务
Node.js 和 Sinatra 是应用框架,并不决定服务如何计费。如果把它们部署在 EC2、传统 VPS 或常驻容器中,进程需要一直运行。即使没有请求,也要支付实例占用时间产生的费用。
邀请 API 通常有这些特点:
- 请求量不稳定;
- 单次处理时间较短;
- 不需要常驻进程;
- 业务容易拆成独立函数;
- 可以接受首次调用稍慢。
这些条件适合使用函数计算。收到请求后,Lambda 会启动处理逻辑,执行结束后释放运行环境,因此不必预先购买一台全年运行的服务器。
不过,按使用量计费不等于费用一定为零。账单可能包含:
- API Gateway 请求和数据传输;
- Lambda 调用次数、执行时间和分配内存;
- CloudWatch Logs 日志存储;
- DynamoDB 读写和存储;
- SES 邮件发送;
- 自定义域名、DNS、WAF 或 NAT Gateway;
- 第三方邮件服务费用。
单价和免费额度会随区域、时间及产品配置变化,应以部署区域的官方价格页面为准。
实现步骤
1. 定义 API
例如创建以下接口:
POST /invites
Content-Type: application/json
Authorization: Bearer <token>
请求体:
{
"email": "friend@example.com"
}
成功响应:
{
"inviteId": "b91ce78e-0bd2-4dd8-8ac7-f764cb03ec43",
"status": "pending"
}
公开 API 至少要验证调用者身份。不能只在前端隐藏接口地址,否则任何人都可能批量调用接口发送垃圾邮件。
2. 编写 Lambda 函数
下面是一个精简的 Node.js 示例,用于校验输入、生成邀请令牌并写入 DynamoDB。示例使用 AWS SDK for JavaScript v3。所选 Lambda 运行时是否预装相关模块,应以对应的运行时说明为准。更稳妥的方式是把依赖放进部署包一起发布。
import crypto from "node:crypto";
import {
DynamoDBClient,
PutItemCommand
} from "@aws-sdk/client-dynamodb";
const dynamodb = new DynamoDBClient({});
const tableName = process.env.INVITES_TABLE;
export const handler = async (event) => {
let body;
try {
body = JSON.parse(event.body ?? "{}");
} catch {
return response(400, { error: "Invalid JSON body" });
}
const email =
typeof body.email === "string"
? body.email.trim().toLowerCase()
: "";
if (!isValidEmail(email)) {
return response(400, { error: "Invalid email address" });
}
// userId 应来自经过验证的 JWT 或 API Gateway authorizer,
// 不应直接相信客户端提交的邀请人身份。
const inviterId = event.requestContext?.authorizer?.jwt?.claims?.sub;
if (!inviterId) {
return response(401, { error: "Unauthorized" });
}
const inviteId = crypto.randomUUID();
const token = crypto.randomBytes(32).toString("base64url");
const createdAt = new Date().toISOString();
await dynamodb.send(
new PutItemCommand({
TableName: tableName,
Item: {
inviteId: { S: inviteId },
inviterId: { S: inviterId },
email: { S: email },
tokenHash: {
S: crypto.createHash("sha256").update(token).digest("hex")
},
status: { S: "pending" },
createdAt: { S: createdAt }
},
ConditionExpression: "attribute_not_exists(inviteId)"
})
);
// 可在此直接调用 SES,也可以将发送任务写入队列后异步处理。
// 邀请链接中的原始 token 只发送给收件人,数据库仅保存其哈希值。
return response(201, {
inviteId,
status: "pending"
});
};
function isValidEmail(value) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
function response(statusCode, body) {
return {
statusCode,
headers: {
"content-type": "application/json; charset=utf-8"
},
body: JSON.stringify(body)
};
}
在生产环境中,不宜完全依靠正则表达式判断电子邮件地址是否合法。完成基本格式校验后,还应发送确认邮件来验证地址归属。
3. 配置 API Gateway
把 POST /invites 路由连接到 Lambda,并完成以下配置:
- JWT authorizer、Cognito authorizer 或其他认证方式;
- 请求体大小限制;
- CORS,只允许需要访问 API 的站点来源;
- 节流和请求配额;
- 统一的错误响应;
- 必要时启用 AWS WAF;
- 分开设置生产环境和测试环境。
如果只需要简单的 HTTP 路由,可以对比 API Gateway 的 HTTP API 和 REST API。两者的功能及价格模型不同。是否需要高级鉴权、请求转换、API Key 和使用计划等能力,会影响具体选择。
4. 授予最小权限
Lambda 的执行角色只应获得完成任务所需的权限。例如,只允许它向指定的邀请表写入数据:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:PutItem"
],
"Resource": "arn:aws:dynamodb:REGION:ACCOUNT_ID:table/Invites"
}
]
}
如果函数还要发送邮件,可以单独添加对应的 SES 权限,并限制可使用的资源和操作。不要为了省事授予 dynamodb:* 或 ses:*。
5. 部署并测试
部署后,可以用 curl 测试:
curl -X POST "https://api.example.com/invites" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
--data '{"email":"friend@example.com"}'
测试至少应覆盖以下情况:
- 合法邀请;
- 未登录访问;
- 无效 JSON;
- 缺少邮箱或邮箱格式错误;
- 重复邀请;
- 超过速率限制;
- DynamoDB 或邮件服务暂时失败;
- 同一个请求被重复提交。
重复请求与异步发送
浏览器重试、网络超时或用户重复点击,都可能让邀请接口创建多条记录。可以要求客户端提供幂等键:
Idempotency-Key: 5d673dde-3d3f-480d-8998-c7d29bcd7682
服务端保存这个键,并通过 DynamoDB 条件写入,保证同一个键只执行一次。另一种方式是用“邀请人 ID + 收件邮箱 + 有效期”建立唯一约束。
邮件发送最好与邀请记录的创建分开处理:
POST /invites
|
v
保存邀请记录
|
v
写入 SQS
|
v
邮件发送 Lambda
|
v
Amazon SES
邮件服务短暂失败时,队列可以安排重试,API 也不必一直等待。接入队列后,要配置最大重试次数和死信队列,防止永久失败的消息反复执行并持续产生费用。
成本与安全注意事项
防止低流量 API 被恶意刷出高账单
按请求计费可以减少空闲成本,但异常流量也会直接推高账单。建议至少采取以下措施:
- 设置 API Gateway 节流;
- 按用户限制邀请频率;
- 设置每日邀请数量上限;
- 配置 AWS Budgets 和账单告警;
- 配置 CloudWatch 指标和异常告警;
- 必要时使用 CAPTCHA 或 WAF;
- 设置邮件服务发送额度并处理退信。
预算告警通常只会发送通知,不会自动停止消费。如果需要强制停用服务,应另行设计自动化控制,并评估误停生产服务的风险。
避免隐藏的固定成本
只使用 API Gateway、Lambda 和 DynamoDB 按需模式时,可以很接近“无请求、低成本”。但以下组件可能持续产生费用:
- NAT Gateway;
- 预置并发;
- 长期保留的大量日志;
- 固定容量数据库;
- 第三方监控套餐;
- Route 53 托管区域和自定义域名相关资源。
如果 Lambda 需要访问公网,但没有必须接入私有子网的理由,不要仅因为觉得这样“更安全”就加入 VPC 和 NAT Gateway。是否需要 VPC,应根据数据库、网络隔离和合规要求判断。
冷启动
长时间没有请求后,第一次调用可能出现额外延迟,这就是常说的冷启动。邀请邮件接口通常可以接受这种延迟。如果接口需要稳定且极低的响应延迟,可能要使用预置并发,但这会减少纯按使用量计费带来的成本优势。
其他可选方案
除了 AWS,还可以考虑 Azure Functions、Google Cloud Functions、Google Cloud Run、Cloudflare Workers,以及部分支持 scale-to-zero 的容器平台。
选择平台时,不能只比较“每百万次请求”的价格,还要确认以下事项:
- 空闲时是否真的能缩容到零;
- 是否存在最低实例数或固定套餐;
- 最大执行时间和内存限制;
- 冷启动表现;
- 数据库连接方式;
- 出站流量费用;
- 自定义域名和日志费用;
- 邮件发送能力;
- 是否支持所需区域并满足合规要求。
对于简单的邀请 API,如果现有网站已经使用 AWS,API Gateway、Lambda、DynamoDB 和 SES 通常是集成成本较低,也比较接近按实际使用量计费的组合。
备注:内容仅供参考。
