
把 NestJS 部署到 AWS Lambda 上跑 API Gateway,听起来挺简单的——直到你开始真正把它们串起来。
从让 NestJS 适配无服务器(Serverless)运行环境,到配置 API 网关(API Gateway),再到用 AWS CDK(云开发工具包)搭建基础设施,这里面有好几个环节容易乱成一锅粥。自己做个人项目的时候,这些坑我一个不落全踩了一遍——尤其是怎么让 NestJS 适配 Lambda 的执行模型。
看完这篇,你将得到:
@codegenie/serverless-express 适配 Lambda 执行模型的 NestJS 应用main 分支自动构建和部署
完整代码可以在 GitHub 上找到。
跟着做的话,你需要准备:
不需要你是 AWS 专家,但对 Lambda 和 API Gateway 怎么配合有个大概印象会让整个过程顺畅很多。
先用 Nest CLI 创建一个新的 NestJS 应用:
npm i -g @nestjs/cli
nest new nestjs-serverless-aws-cdk
这会生成一个标准的 NestJS 项目,包含所有必要的模板代码。
想深入了解 NestJS 的概念和架构,强烈建议看看 NestJS 官方文档。
现在启动应用:
cd nestjs-serverless-aws-cdk
npm run start
应用应该在 http://localhost:3000/ 上跑起来了。默认端口在 src/main.ts 里配置:
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
脚手架生成的项目自带:
app.controller.ts)app.service.ts)app.module.ts)既然这篇是讲部署的,我们就直接用默认的端点,不再新建了。
默认情况下,NestJS 应用跑在一个长期运行的 HTTP 服务器上。它启动一次、初始化依赖,然后持续处理请求。
而 AWS Lambda 走的是事件驱动模型,每次请求在一个短周期的无状态环境中执行(偶尔会有冷启动)。
因为这个差异,NestJS 不能直接跑在 Lambda 上,必须做点适配工作。为了弥合这个差距,我们用 @codegenie/serverless-express,它能让 NestJS 应用在 AWS Lambda 里运行。
它底层做的事:
简单说,就是让 Lambda 表现得像个 HTTP 服务器,这样 NestJS 就能跑起来,不用大改代码。
搞清楚了为什么要加这层配置,现在来装需要的包:
npm install @codegenie/serverless-express @types/aws-lambda
注意:
@types/aws-lambda提供了处理器里用的 TypeScript 类型定义。
然后在 src/ 目录(和 main.ts 同级)新建一个文件叫 lambda.ts,这就是 Lambda 函数的入口。
// src/lambda.ts
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import serverlessExpress from '@codegenie/serverless-express';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context,
} from 'aws-lambda';
import express from 'express';
import { AppModule } from './app.module'; type AsyncHandler = ( event: APIGatewayProxyEvent, context: Context,
) => Promise<APIGatewayProxyResult>; let serverlessExpressInstance: AsyncHandler | undefined; async function setup( event: APIGatewayProxyEvent, context: Context,
): Promise<APIGatewayProxyResult> { const expressApp = express(); const nestApp = await NestFactory.create( AppModule, new ExpressAdapter(expressApp), ); nestApp.enableCors(); await nestApp.init(); serverlessExpressInstance = serverlessExpress({ app: expressApp, }) as unknown as AsyncHandler; return serverlessExpressInstance(event, context);
} export function handler( event: APIGatewayProxyEvent, context: Context,
): Promise<APIGatewayProxyResult> { if (serverlessExpressInstance) { return serverlessExpressInstance(event, context); } return setup(event, context);
}
上面代码里有几个关键点值得说一下:
serverlessExpressInstance 在处理器外声明,只在第一次调用时才初始化。在同一个 Lambda 容器内的后续调用中,if (serverlessExpressInstance) 检查会跳过完整的 NestJS 启动过程,复用已有实例。这是 Lambda 里减少冷启动开销的标准写法。setup 函数里是 NestJS 启动的地方。它创建 Express 应用,用 NestJS 的 ExpressAdapter 包装,再传给 serverlessExpress。生成的实例是一个函数,接收 Lambda 事件,返回 Lambda 兼容的响应。这个处理器用的是标准的 async/await 模式,支持新版 AWS Lambda Node.js 运行时(包括 Node.js 20 和 24)。
AWS Lambda Node.js 24 运行时已经不支持回调风格的处理器(
callback(null, response))了——用了会导致函数一直挂起直到超时。具体看 AWS Lambda Node.js 运行时文档。
Lambda 处理器已经ready了,现在来用 AWS CDK 搭建基础设施。AWS 云开发工具包(CDK)是一个基础设施即代码(IaC)框架,可以用 TypeScript、Python、Java 等编程语言定义 AWS 资源。换句话说:你不用手写 CloudFormation 模板,而是写代码,这些代码会被合成为 CloudFormation 模板,再用来部署资源到 AWS。
CDK 的流程大概是这样的:
👨💻 写 CDK 代码 ↓
🧪 cdk synth ↓
📄 CloudFormation 模板 ↓
🚀 cdk deploy ↓
☁️ AWS 资源
实际上 CDK 让基础设施更容易理解和维护,比手写模板舒服多了。
在定义基础设施之前,先了解一下 CDK 的几个核心概念有帮助:
这篇会定义一个 Stack,配置 Lambda 函数、API Gateway 和相关资源。
搞清了 AWS CDK 的基本概念,现在在项目里初始化。把基础设施代码和 NestJS 源码分开,初始化到一个子目录 infra/ 里。
在项目根目录运行:
mkdir infra && cd infra
npx cdk init app --language typescript
这会生成基本的 CDK 项目结构:
.
├── bin/
├── lib/
├── cdk.json
├── package.json
└── tsconfig.json
其中:
bin/ 包含 CDK 应用的入口lib/ 包含基础设施的堆栈定义cdk.json 包含 CDK CLI 的配置在运行任何 AWS CDK 命令之前,需要在本地机器上配置 AWS 凭证。
这样 AWS 才能认证你的请求,替你执行操作——比如创建 CloudFormation 堆栈、上传资源。
比如你运行:
npx cdk bootstrap
你的 IAM 身份需要有权限来创建基础资源,比如:
在生产环境里,最好遵循最小权限原则,只授予特定任务所需的权限。
不过为了简单起见,这篇用了一个附加了 AdministratorAccess 策略的 IAM 用户。
按这些步骤配置 AWS 凭证:
确保机器上装了 AWS CLI,参考官方安装指南。
创建一个带 AdministratorAccess 策略的 IAM 用户,并生成访问密钥。
cdk-bootstrap-admin)
记得下载密钥备用——之后没法再查看了。
运行 aws configure 来设置 AWS CLI 的凭证:
aws configure
会提示输入:
AWS Access Key ID:
AWS Secret Access Key:
Default region name: us-east-1
Default output format: json
填上上一步生成的访问密钥。
运行下面的 AWS CLI 命令确认凭证配置正确,会显示你的 AWS 账号和 IAM 用户信息:
aws sts get-caller-identity
输出:
{ "UserId": "UserId", "Account": "AccountNumber", "Arn": "arn:aws:iam::<AccountNumber>:user/cdk-bootstrap-admin"
}
AWS 凭证在本地配好了,现在来引导 CDK 环境。
引导操作是为 CDK 准备 AWS 账号,这样 CDK 才能帮你部署资源。
在 infra/ 目录运行:
npx cdk bootstrap
这条命令会在你的 AWS 账号里创建一个一次性的 CloudFormation 堆栈(叫 CDKToolkit),用来设置 IAM 角色和 S3 存储桶来上传 CloudFormation 模板。这些资源是后续 CDK 部署必须的。
每个账号和区域只需要运行一次
cdk bootstrap。
环境引导完了,现在来用 AWS CDK 定义基础设施。这节要创建一个堆栈来配置应用的 Lambda 函数。
我还用了 Lambda 层来打包生产依赖。把 node_modules 和应用代码分开,能减小部署包体积,也让同一个层可以被多个 Lambda 函数复用。
进入 infra/lib 目录,找到初始化 CDK 项目时生成的文件 infra-stack.ts。可以重命名,但这篇保持原样。
在 infra/lib/infra-stack.ts 添加下面的堆栈定义代码:
// infra/lib/infra-stack.ts
import * as cdk from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2';
import * as integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
import * as path from 'path'; export class InfraStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const nodeModulesLayer = new lambda.LayerVersion(this, 'NodeModulesLayer', { code: lambda.Code.fromAsset(path.join(__dirname, '../../layer')), compatibleRuntimes: [lambda.Runtime.NODEJS_24_X], description: 'Production node_modules for NestJS Lambda', }); const nestApiLambda = new lambda.Function(this, 'NestApiLambdaFunction', { runtime: lambda.Runtime.NODEJS_24_X, handler: 'src/lambda.handler', code: lambda.Code.fromAsset(path.join(__dirname, '../../dist'), { exclude: ['infra', 'tsconfig*'], }), memorySize: 512, layers: [nodeModulesLayer], }); const httpApi = new apigwv2.HttpApi(this, 'HttpApi', { defaultIntegration: new integrations.HttpLambdaIntegration( 'LambdaIntegration', nestApiLambda, ), }); new cdk.CfnOutput(this, 'HttpApiUrl', {