Node 与 Hono 部署到 Lambda

# Node 与 Hono 部署到 Lambda

业务代码保持一致,本地用 HTTP Server 接请求,云上用 Lambda handler 接事件。迁移重点是入口适配、打包、配置、网络和发布验证,不是重写所有接口。

# 先看请求怎样到达代码

浏览器 → API Gateway HTTP API
  → Lambda handler
  → Hono 中间件与路由
  → 业务服务 → 数据库/第三方 API
1
2
3
4

API Gateway 把 HTTP 请求转换成事件,适配器再把它转换为 Hono 能处理的请求,并将响应转回 Lambda 返回格式。API Gateway 负责入口不代表它自动完成全部业务鉴权。

# API Gateway 与 Function URL 怎么选

Function URL 给 Lambda 一个可访问的 HTTPS 地址;API Gateway 在后端前面增加一层 API 管理能力。两者都能用于生产,按功能需求选择,不按开发或生产环境划分。

对比项 Function URL (opens new window) API Gateway HTTP API (opens new window)
请求去哪里 一个 Lambda;函数内可以用 Hono 处理多个业务路径 可按路径和方法转发到不同 Lambda 或 HTTP 后端
自定义域名 不直接支持,可在前面配 CloudFront 直接支持配置
鉴权 内置 AWS_IAM 或 NONE;业务登录可在代码里处理 支持 IAM、JWT 或 Lambda 授权器,在网关检查后再放行
限流与日志 可用 Lambda 并发限制保护函数;访问明细需自行记录或借助前置服务 可配置 Stage/路由请求限流与访问日志
配置与费用 配置少,URL 本身不额外收费;Lambda 等用量仍收费 管理能力更丰富,但增加配置和网关费用

怎么选:只需要公开一个 Lambda API 或 Webhook,优先考虑 Function URL;需要直接配置自定义域名、请求限流、访问日志、授权器或多后端路由,优先考虑 API Gateway。不是这些功能只能用 API Gateway 实现,而是不用自己组合多套服务。

例如,Hono 已经能处理 /tasks、/agents 和登录校验,这本身不需要 API Gateway;如果还希望在业务代码之外统一配置接口限流、域名和访问日志,增加网关才有明确收益。Lambda 并发限制控制同时执行多少请求,不等于按每秒请求数设置入口限流。

API Gateway 还有 REST API 产品,提供 API Key 使用计划、请求校验和更丰富的请求转换等能力。HTTP API 与 REST API 是不同产品类型,不能把 REST API 独有功能当作 HTTP API 也支持。

Stage:API 的部署入口

Stage(部署阶段)是 API Gateway 中关联某份 API 部署的访问入口,常用 dev、prod 区分开发和生产,例如默认网关地址下的 /dev/tasks、/prod/tasks。每个 Stage 可以配置访问日志和限流;HTTP API 的 $default Stage 则不需要在路径中带阶段名称。

不同 Stage 不等于后端自动隔离:开发和生产还需要连接各自的后端服务与数据库。

NONE 只表示入口不要求 IAM 签名,不代表业务请求无需鉴权。公开入口仍要有应用鉴权、限流、日志和并发保护;CORS 只是浏览器读取策略,不能阻止脚本或服务端直接调用。需要 WAF 时还要选择受支持的接入方式,不能假定 Function URL 或 HTTP API 可以直接绑定 Web ACL。

# 一套业务,两个入口

下面用于安装了 hono、@hono/node-server 的 TypeScript ESM 项目。三个文件完整展示入口关系;src/app.ts:

import { Hono } from 'hono';

// 只装配业务与中间件,不在此处监听端口。
export const app = new Hono();
app.get('/healthz', (c) => c.json({ status: 'ok' }));
app.get('/hello', (c) => c.json({ message: '你好,Lambda' }));
1
2
3
4
5
6

src/local.ts:

import { serve } from '@hono/node-server';
import { app } from './app.js';

// 本地入口启动常驻 HTTP Server;不作为 Lambda handler 打包。
serve({ fetch: app.fetch, port: 3000 });
1
2
3
4
5

src/lambda.ts:

import { handle } from 'hono/aws-lambda';
import { app } from './app.js';

// Lambda 调用导出的 handler;云上无需自行 listen 端口。
export const handler = handle(app);
1
2
3
4
5

本地用项目锁定的 tsx 执行 src/local.ts;云端打包 src/lambda.ts,并使函数 handler 配置与产物文件名、导出名一致。生产中还需超时、输入校验、鉴权、日志与错误映射。

# 从本地运行到云端验收

  1. 准备产物。锁定 Node 与依赖版本,打包入口及 workspace 依赖,检查原生依赖与目标 CPU 架构是否匹配。
  2. 验证最终包。不只测源码,还在目标 Node 环境实际 import 打包后的 .mjs,检查模块初始化错误。
  3. 声明资源。用 SAM/CloudFormation 定义函数、日志、HTTP API、执行角色、网络和配置,不手动维护两套状态。
  4. 审查变更。核对入口、handler、代码 hash、网络、权限、timeout 和环境变量;Secret 不进入模板正文。
  5. 发布并验证。等待 Stack 完成,再检查函数状态、真实 API、数据库和第三方调用。函数部署成功不等于业务链路成功。

下面是已有项目中的只读验证操作示例,不创建或更新云资源。先把 profile、Region 和 Stack 名替换成已经核对的目标:

# 身份检查先于资源查询;如发现是 root 或错误账号,停止后续操作。
aws sts get-caller-identity --profile notes-readonly
# 查看部署状态,不输出可能包含敏感值的全部 Parameters/Outputs。
aws cloudformation describe-stacks \
  --profile notes-readonly --region us-east-2 \
  --stack-name example-api \
  --query 'Stacks[0].{Name:StackName,Status:StackStatus}'
1
2
3
4
5
6
7

# 配置、连接池和超时

环境变量不是自动从本地 .env 上传的;由部署配置或 Secret 注入。启动阶段校验缺失参数,但错误日志只输出字段名,不输出值。

Lambda 执行环境可能复用,也可能被销毁,所以内存与 /tmp 只能用于临时数据或缓存,不能作为业务状态的唯一存储。初始化与冷启动的原理见 Lambda 运行机制。

可以把数据库 Pool 放在模块级复用执行环境,不能每次请求无限新建连接;也不能把用户身份或请求数据留在全局变量里。总连接量约受执行环境数量与每环境连接池上限共同影响,扩容前先确认数据库承载能力,必要时使用适配的连接代理。

请求链路中,数据库和第三方调用的超时应给应用留出错误处理余量,再由函数与网关设置更外层边界。长任务走队列与状态查询,不把调大 timeout 当成唯一方案。

# 用数字估算并发和连接压力

稳定负载下,可以用 “每秒请求数 × 平均执行秒数” 粗估正在执行的请求数。比如每秒 100 个请求、平均执行 0.2 秒,平均并发约 20;如果下游变慢到 2 秒,请求量没变,并发也可能涨到约 200。它是平均量估算,不是突发容量保证。

如果 200 个执行环境各自允许 5 条数据库连接,理论连接上限就可能接近 1000;还没算其他服务和发布时的新旧环境。因此模块级连接池只能减少重复建连,不能自动限制整个系统的总连接。应结合函数并发上限、较小的单环境池、查询耗时和连接代理共同控制。

预置并发和保留并发不是一回事

Provisioned concurrency(预置并发)预先准备执行环境,用来减少相应请求的冷启动等待;reserved concurrency(保留并发)为函数预留并限制可用并发。保留并发不负责预热,预置并发也不能保证超出预置容量的请求没有冷启动。具体成本和配置关系应按目标函数评估。

# 超时需要逐层收口

假设接口体验预算是 3 秒,可以为一次数据库查询设更短上限,并给序列化、日志及错误返回留时间;不能让每个串行下游都各用满 3 秒。客户端取消后,还需将取消信号传给支持取消的下游调用;函数 timeout 是最外层兜底,不是优雅取消方案。

写请求超时尤其危险:数据库可能已提交,只是响应未送达。客户端用同一业务幂等键查询或重试,后端返回已保存的结果;不能重新生成操作 ID 再创建第二份业务记录。具体事务方式见 消息可靠处理。

冷启动优化先测耗时分布:包加载、模块初始化、Secret 获取和首次建连各花多久,再决定精简依赖、复用客户端或预置容量。把网络请求全部放在模块初始化里,可能拖慢每个新环境的就绪,不是无条件的优化。

# 从原项目复用的排障经验

症状 核对什么 处理原则
源码测试通过,冷启动报 ESM 错误 最终 bundle 的 import、CJS 适配与 banner 命名 在目标运行时 import 产物;避免重复注入同名 createRequire
进入 VPC 后访问外部 API 超时 私网子网 NAT/Endpoint 路由、DNS 和安全组 进 VPC 为了访问私网,不会自动获得公网出口
浏览器显示 CORS 错误 网关、Lambda 是否在中间件之前返回 5xx 先看真实 HTTP 状态,再查 Origin,不直接放开 *
修改前端 API 地址后没有生效 是否为构建时注入的公开变量 重新构建前端,不能只改服务器环境变量
显示部署成功但请求仍是旧版 API integration 是否调用正确版本或 alias 继续核对实际流量入口与版本,不只看代码包上传结果

# 面试时可以这样回答

Hono 部署到 Lambda,需要改多少业务代码?参考答案

如果业务和启动入口已分开,主要是增加 Lambda handler 适配,路由和服务可以复用。但上线还要处理打包、环境变量、VPC、数据库连接池和超时。适配器只解决请求格式转换,不能替代完整的部署与运行验证。

Lambda 自动扩容,为什么还要限制并发?参考答案

函数的扩容速度可能超过数据库和外部 API 的承受能力。每个环境都有连接池,环境一多,总连接就放大;下游变慢还会使并发继续堆积。我会按下游容量设并发上限、连接预算和超时,异步任务用队列缓冲,而不是让自动扩容把瓶颈压垮。

怎么区分冷启动慢和业务本身慢?参考答案

我把初始化时间与 handler 内的数据库、外部调用耗时分开,并按版本观察新环境和复用环境。如果只有首次包加载慢,就精简依赖或评估预置并发;如果每次都慢,重点查查询和下游。预热无法解决慢 SQL,也不能代替端到端追踪。

# 复用来源与参考

整理自 github-account-info 的 Node Lambda 部署实录;项目里的具体云参数不作为通用默认值。