site logo

Marico's space

如何使用 CDK 和 GitHub Actions 将 NestJS 部署到 AWS Lambda

服务器技术 2026-05-08 17:37:47 1

把 NestJS 部署到 AWS Lambda 上跑 API Gateway,听起来挺简单的——直到你开始真正把它们串起来。

从让 NestJS 适配无服务器(Serverless)运行环境,到配置 API 网关(API Gateway),再到用 AWS CDK(云开发工具包)搭建基础设施,这里面有好几个环节容易乱成一锅粥。自己做个人项目的时候,这些坑我一个不落全踩了一遍——尤其是怎么让 NestJS 适配 Lambda 的执行模型。

看完这篇,你将得到:

  • @codegenie/serverless-express 适配 Lambda 执行模型的 NestJS 应用
  • 用 AWS CDK 定义的基础设施:Lambda 函数和 HTTP API 网关
  • GitHub Actions 工作流,每次推送到 main 分支自动构建和部署
NestJS 部署到 AWS Lambda 的简单架构图

完整代码可以在 GitHub 上找到。

前提条件

跟着做的话,你需要准备:

  • 一个 AWS 账号
  • 对 Node.js 和 NestJS 有基本了解
  • 对 AWS 服务和工具(AWS CLI、IAM、Lambda、API Gateway、CloudFormation)有基本了解
  • 一个 GitHub 账号(用来创建仓库和配置 GitHub Actions)

不需要你是 AWS 专家,但对 Lambda 和 API Gateway 怎么配合有个大概印象会让整个过程顺畅很多。

创建新的 NestJS 项目

先用 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 适配 AWS Lambda

默认情况下,NestJS 应用跑在一个长期运行的 HTTP 服务器上。它启动一次、初始化依赖,然后持续处理请求。

而 AWS Lambda 走的是事件驱动模型,每次请求在一个短周期的无状态环境中执行(偶尔会有冷启动)。

因为这个差异,NestJS 不能直接跑在 Lambda 上,必须做点适配工作。为了弥合这个差距,我们用 @codegenie/serverless-express,它能让 NestJS 应用在 AWS Lambda 里运行。

它底层做的事:

  • 把 Lambda 事件转换成 HTTP 请求
  • 传给 NestJS 应用处理
  • 把响应转换回 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 运行时文档。

用 AWS CDK 搭建基础设施

Lambda 处理器已经ready了,现在来用 AWS CDK 搭建基础设施。AWS 云开发工具包(CDK)是一个基础设施即代码(IaC)框架,可以用 TypeScript、Python、Java 等编程语言定义 AWS 资源。换句话说:你不用手写 CloudFormation 模板,而是写代码,这些代码会被合成为 CloudFormation 模板,再用来部署资源到 AWS。

CDK 的流程大概是这样的:

👨‍💻 写 CDK 代码 ↓
🧪 cdk synth ↓
📄 CloudFormation 模板 ↓
🚀 cdk deploy ↓
☁️ AWS 资源

实际上 CDK 让基础设施更容易理解和维护,比手写模板舒服多了。

CDK 核心概念速览

在定义基础设施之前,先了解一下 CDK 的几个核心概念有帮助:

  • App → CDK 应用的根容器
  • Stack → 部署单元(对应一个 CloudFormation 堆栈)
  • Construct → 用来定义 AWS 资源的构建块

这篇会定义一个 Stack,配置 Lambda 函数、API Gateway 和相关资源。

在项目中初始化 CDK

搞清了 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 凭证(步骤)

在运行任何 AWS CDK 命令之前,需要在本地机器上配置 AWS 凭证。

这样 AWS 才能认证你的请求,替你执行操作——比如创建 CloudFormation 堆栈、上传资源。

比如你运行:

npx cdk bootstrap

你的 IAM 身份需要有权限来创建基础资源,比如:

  • CloudFormation 堆栈
  • S3 存储桶(用来存资源)
  • IAM 角色和策略

在生产环境里,最好遵循最小权限原则,只授予特定任务所需的权限。

不过为了简单起见,这篇用了一个附加了 AdministratorAccess 策略的 IAM 用户。

按这些步骤配置 AWS 凭证:

1. 安装 AWS CLI

确保机器上装了 AWS CLI,参考官方安装指南。

2. 创建 IAM 用户

创建一个带 AdministratorAccess 策略的 IAM 用户,并生成访问密钥。

  1. 登录 AWS 控制台
  2. 导航到 IAM → 用户 → 创建用户
  3. 创建用户(比如 cdk-bootstrap-admin
    • 附加 AdministratorAccess 策略
    • 生成 访问密钥

记得下载密钥备用——之后没法再查看了。

3. 运行 AWS Configure

运行 aws configure 来设置 AWS CLI 的凭证:

aws configure

会提示输入:

AWS Access Key ID:
AWS Secret Access Key:
Default region name: us-east-1
Default output format: json

填上上一步生成的访问密钥。

4. 验证凭证配置

运行下面的 AWS CLI 命令确认凭证配置正确,会显示你的 AWS 账号和 IAM 用户信息:

aws sts get-caller-identity

输出:

{ "UserId": "UserId", "Account": "AccountNumber", "Arn": "arn:aws:iam::<AccountNumber>:user/cdk-bootstrap-admin"
}

引导 CDK 项目

AWS 凭证在本地配好了,现在来引导 CDK 环境。

引导操作是为 CDK 准备 AWS 账号,这样 CDK 才能帮你部署资源。

infra/ 目录运行:

npx cdk bootstrap

这条命令会在你的 AWS 账号里创建一个一次性的 CloudFormation 堆栈(叫 CDKToolkit),用来设置 IAM 角色和 S3 存储桶来上传 CloudFormation 模板。这些资源是后续 CDK 部署必须的。

CDKToolkit CloudFormation 堆栈创建的资源截图

每个账号和区域只需要运行一次 cdk bootstrap

编写 CDK 堆栈

环境引导完了,现在来用 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', {