Smithy TypeScript API
Smithy 是一种与协议无关的接口定义语言,用于以模型驱动的方式编写 API。
Smithy TypeScript API 生成器使用 Smithy 进行服务定义,并使用 Smithy TypeScript Server SDK 进行实现来创建新的 API。该生成器提供 CDK 或 Terraform 基础设施即代码,将您的服务部署到 AWS Lambda,并通过 AWS API Gateway REST API 公开。它提供类型安全的 API 开发,并从 Smithy 模型自动生成代码。生成的处理程序使用 AWS Lambda Powertools for TypeScript 进行可观测性,包括日志记录、AWS X-Ray 跟踪和 CloudWatch 指标
生成 Smithy TypeScript API
Section titled “生成 Smithy TypeScript API”您可以通过两种方式生成新的 Smithy TypeScript API:
pnpm nx g @aws/nx-plugin:ts#api --framework=smithyyarn nx g @aws/nx-plugin:ts#api --framework=smithynpx nx g @aws/nx-plugin:ts#api --framework=smithybunx nx g @aws/nx-plugin:ts#api --framework=smithy您还可以执行试运行以查看哪些文件会被更改
pnpm nx g @aws/nx-plugin:ts#api --framework=smithy --dry-runyarn nx g @aws/nx-plugin:ts#api --framework=smithy --dry-runnpx nx g @aws/nx-plugin:ts#api --framework=smithy --dry-runbunx nx g @aws/nx-plugin:ts#api --framework=smithy --dry-run- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - ts#api - 填写必需参数
- framework: smithy
- 点击
Generate
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| name 必需 | string | - | API 的名称(必填)。用于生成类名和文件路径。 |
| framework | trpc | smithy | trpc | 要使用的API框架。 |
| namespace | string | - | Smithy API 的命名空间(仅适用于 smithy 框架)。默认为您的 monorepo 作用域 |
| integrationPattern | isolated | shared | isolated | API Gateway 集成的生成方式。可选择 isolated(默认)或 shared。 |
| auth | iam | cognito | custom | iam | 用于对 API 进行身份验证的方法。可选择 iam(默认)、cognito 或 custom。 |
| directory | string | packages | 存储应用程序的目录。 |
| subDirectory | string | - | 项目所在的子目录。默认情况下为项目名称。 |
| iac | inherit | cdk | terraform | inherit | 首选的 IaC 提供商。默认情况下,这将继承您的初始选择。 |
| infra | rest-lambda | http-lambda | none | rest-lambda | 用于部署此 API 的基础设施类型。 |
| preferInstallDependencies | boolean | true | 是否在生成器运行后优先安装依赖项。设置为 false 可在批量运行多个生成器时延迟安装(如果后续生成器需要计算 Nx 项目图,仍会运行安装);在最后统一安装一次。 |
生成器在 <directory>/<api-name> 目录中创建两个相关项目:
文件夹model/ Smithy 模型项目
- package.json 定义项目包名称和依赖项的项目清单
- project.json 项目配置和构建目标
- smithy-build.json Smithy 构建配置
- ssdk.rolldown.config.mjs 打包生成的 TypeScript Server SDK
文件夹src/
- main.smithy 主服务定义
文件夹operations/
- echo.smithy 示例操作定义
文件夹backend/ TypeScript 后端实现
- project.json 项目配置和构建目标
- rolldown.config.ts 打包配置
文件夹src/
- handler.ts AWS Lambda 处理程序
- local-server.ts 本地开发服务器
- service.ts 服务实现
- context.ts 服务上下文定义
文件夹operations/
- echo.ts 示例操作实现
文件夹generated/ 生成的 TypeScript SDK(在构建期间创建)
- …
由于此生成器根据您选择的 iac 创建基础设施即代码,它将在 packages/common 中创建一个项目,其中包含相关的 CDK 构造或 Terraform 模块。
通用基础设施即代码项目的结构如下:
文件夹packages/common/constructs
文件夹src
文件夹app/ 特定于项目/生成器的基础设施构造
文件夹apis/
- <project-name>.ts 用于部署 API 的 CDK 构造
文件夹core/ 由
app中的构造重用的通用构造文件夹api/
- rest-api.ts 用于部署 REST API 的 CDK 构造
- utils.ts API 构造的实用工具
- index.ts 从
app导出构造的入口点
- project.json 项目构建目标和配置
文件夹packages/common/terraform
文件夹src
文件夹app/ 特定于项目/生成器的基础设施 Terraform 模块
文件夹apis/
文件夹<project-name>/
- <project-name>.tf 用于部署 API 的模块
文件夹core/ 由
app中的模块重用的通用模块文件夹api/
文件夹rest-api/
- rest-api.tf 用于部署 REST API 的模块
- project.json 项目构建目标和配置
部署的 Smithy API 具有以下架构,在 API Gateway 阶段前面有一个 AWS WAFv2 Web ACL:
实现您的 Smithy API
Section titled “实现您的 Smithy API”在 Smithy 中定义操作
Section titled “在 Smithy 中定义操作”操作在模型项目中的 Smithy 文件中定义。主服务定义在 main.smithy 中:
$version: "2.0"
namespace your.namespace
use aws.protocols#restJson1use smithy.framework#ValidationException
@title("YourService")@restJson1service YourService { version: "1.0.0" operations: [ Echo, // Add your operations here ] errors: [ ValidationException ]}各个操作在 operations/ 目录中的单独文件中定义:
$version: "2.0"
namespace your.namespace
@http(method: "POST", uri: "/echo")operation Echo { input: EchoInput output: EchoOutput}
structure EchoInput { @required message: String
foo: Integer bar: String}
structure EchoOutput { @required message: String}如果您有多个共享相同数据类型的 Smithy API,您可以在形状库中定义这些类型一次,而不是在每个模型中重复它们。形状库是一个没有服务的 Smithy 项目 - 只有可重用的形状 - 任意数量的 Smithy 项目都可以依赖它。
使用 smithy#project 生成器生成一个:
pnpm nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapesyarn nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapesnpx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapesbunx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes您还可以执行试运行以查看哪些文件会被更改
pnpm nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes --dry-runyarn nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes --dry-runnpx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes --dry-runbunx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes --dry-run- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - smithy#project - 填写必需参数
- name: my-shapes
- type: shapes
- 点击
Generate
然后,您的 API 模型可以使用 use 引用其形状:
$version: "2.0"
namespace com.example.api
use com.example.shared#Customer
structure GetCustomerOutput { @required customer: Customer}请参阅 Smithy 项目指南,了解如何创建形状库并将其连接为 API 模型的依赖项。
在 TypeScript 中实现操作
Section titled “在 TypeScript 中实现操作”操作实现位于后端项目的 src/operations/ 目录中。每个操作都使用从 TypeScript Server SDK 生成的类型实现(在构建时从您的 Smithy 模型生成)。
import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input) => { // Your business logic here return { message: `Echo: ${input.message}` // type-safe based on your Smithy model };};操作必须在 src/service.ts 中注册到服务定义:
import { ServiceContext } from './context.js';import { YourServiceService } from './generated/ssdk/index.js';import { Echo } from './operations/echo.js';// Import other operations here
// Register operations to the service hereexport const Service: YourServiceService<ServiceContext> = { Echo, // Add other operations here};您可以在 context.ts 中为操作定义共享上下文:
export interface ServiceContext { // Powertools tracer, logger and metrics are provided by default tracer: Tracer; logger: Logger; metrics: Metrics; // Add shared dependencies, database connections, etc. dbClient: any; userIdentity: string;}此上下文传递给所有操作实现,可用于共享资源,如数据库连接、配置或日志记录实用工具。
使用 AWS Lambda Powertools 进行可观测性
Section titled “使用 AWS Lambda Powertools 进行可观测性”生成器使用 AWS Lambda Powertools 配置结构化日志记录,并通过 Middy 中间件自动注入上下文。
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);您可以通过上下文从操作实现中引用日志记录器:
import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => { ctx.logger.info('Your log message'); // ...};AWS X-Ray 跟踪通过 captureLambdaHandler 中间件自动配置。
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);您可以在操作中向跟踪添加自定义子段:
import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => { // Creates a new subsegment const subsegment = ctx.tracer.getSegment()?.addNewSubsegment('custom-operation'); try { // Your logic here } catch (error) { subsegment?.addError(error as Error); throw error; } finally { subsegment?.close(); }};CloudWatch 指标通过 logMetrics 中间件自动为每个请求收集。
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);您可以在操作中添加自定义指标:
import { MetricUnit } from '@aws-lambda-powertools/metrics';import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => { ctx.metrics.addMetric("CustomMetric", MetricUnit.Count, 1); // ...};Smithy 提供内置的错误处理。您可以在 Smithy 模型中定义自定义错误:
@error("client")@httpError(400)structure InvalidRequestError { @required message: String}并将它们注册到您的操作/服务:
operation MyOperation { ... errors: [InvalidRequestError]}然后在 TypeScript 实现中抛出它们:
import { InvalidRequestError } from '../generated/ssdk/index.js';
export const MyOperation: MyOperationHandler<ServiceContext> = async (input) => { if (!input.requiredField) { throw new InvalidRequestError({ message: "Required field is missing" }); }
return { /* success response */ };};访问调用用户
Section titled “访问调用用户”当您的 API 受身份验证保护时,您的操作通常需要知道谁在调用。推荐的方法是在处理程序中解析一次调用者的身份,并通过服务上下文传递它以供特定操作使用。
我们将未授权的情况建模为 Smithy 错误,以便它序列化为正确的 403 响应。将其添加到您的模型中,例如在 model/src/operations/errors.smithy 中,并在任何需要身份的操作上引用它:
$version: "2.0"
namespace your.namespace
/// Thrown when the calling user cannot be determined@error("client")@httpError(403)structure UnauthorizedError { @required message: String}首先,在 src/context.ts 中的服务上下文上公开已解析的身份。我们将其作为函数提供,以便从操作内部抛出 UnauthorizedError(Server SDK 将其序列化为 403),而不是从处理程序中抛出:
import { Logger } from '@aws-lambda-powertools/logger';import { Metrics } from '@aws-lambda-powertools/metrics';import { Tracer } from '@aws-lambda-powertools/tracer';
export interface Identity { sub: string; username: string;}
/** * Context provided to all operations. */export interface ServiceContext { tracer: Tracer; logger: Logger; metrics: Metrics; getIdentity: () => Promise<Identity>;}接下来,在 src/identity.ts 中编写解析器。当无法确定调用者时,它会抛出 UnauthorizedError。实现取决于您选择的 auth 方法:
对于 IAM 身份验证,我们使用从 API Gateway 事件中提取的 sub 在 Cognito 中查找调用者:
import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';import type { APIGatewayProxyEvent } from 'aws-lambda';import { Identity } from './context.js';import { UnauthorizedError } from './generated/ssdk/index.js';
const cognito = new CognitoIdentityProvider();
export const getIdentity = async ( event: APIGatewayProxyEvent,): Promise<Identity> => { const cognitoAuthenticationProvider = event.requestContext?.identity?.cognitoAuthenticationProvider;
let sub: string | undefined = undefined; if (cognitoAuthenticationProvider) { const providerParts = cognitoAuthenticationProvider.split(':'); sub = providerParts[providerParts.length - 1]; }
if (!sub) { throw new UnauthorizedError({ message: 'Unable to determine calling user' }); }
const { Users } = await cognito.listUsers({ // Assumes user pool id is configured in lambda environment UserPoolId: process.env.USER_POOL_ID!, Limit: 1, Filter: `sub="${sub}"`, });
if (!Users || Users.length !== 1) { throw new UnauthorizedError({ message: `No user found with subjectId ${sub}` }); }
return { sub, username: Users[0].Username! };};使用 auth: 'cognito',API Gateway Cognito User Pools 授权器验证调用者在 Authorization 标头中提供的 JWT,并将验证的声明放在事件的 event.requestContext.authorizer.claims 上:
import type { APIGatewayProxyEvent } from 'aws-lambda';import { Identity } from './context.js';import { UnauthorizedError } from './generated/ssdk/index.js';
export const getIdentity = async ( event: APIGatewayProxyEvent,): Promise<Identity> => { const claims = event.requestContext?.authorizer?.claims as | Record<string, string> | undefined;
const sub = claims?.sub; const username = claims?.username;
if (!sub || !username) { throw new UnauthorizedError({ message: 'Unable to determine calling user' }); }
return { sub, username };};然后在 src/handler.ts 中将解析器连接到上下文:
import { Service } from './service.js';import { getIdentity } from './identity.js';// ...const httpResponse = await serviceHandler.handle(httpRequest, { tracer, logger, metrics, getIdentity: () => getIdentity(event),});现在我们可以在操作中使用已解析的身份,例如在 src/operations/echo.ts 中:
import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => { const identity = await ctx.getIdentity(); return { message: `${identity.username} says ${input.message}` };};构建和代码生成
Section titled “构建和代码生成”Smithy 模型项目使用 Smithy CLI 构建 Smithy 工件并生成 TypeScript Server SDK:
pnpm nx build <model-project>yarn nx build <model-project>npx nx build <model-project>bunx nx build <model-project>在 macOS 和 Linux 上,CLI 由 mise 解析,构建会按需获取它,因此无需安装任何东西 - 它会在您第一次构建时下载并缓存固定版本。
此过程:
- 编译 Smithy 模型并验证它
- 从 Smithy 模型生成 OpenAPI 规范
- 创建 TypeScript Server SDK,具有类型安全的操作接口
- 将构建工件输出到
dist/<model-project>/build/
后端项目在编译期间自动复制生成的 SDK:
pnpm nx copy-ssdk <backend-project>yarn nx copy-ssdk <backend-project>npx nx copy-ssdk <backend-project>bunx nx copy-ssdk <backend-project>在 Windows 上构建
Section titled “在 Windows 上构建”mise 不向 npm 发布 Windows 包,因此在 Windows 上,Smithy CLI 是您自己安装的先决条件。按照 Smithy CLI 安装指南安装一次(例如 winget install smithy 或 scoop install smithy),并确保 smithy 在您的 PATH 上。在 Windows 上生成的 Smithy 项目直接运行 smithy,而不是通过 mise。
或者,在 WSL 内开发,其中构建运行 Linux 路径,mise 为您解析 CLI - 无需安装任何东西。
在 Windows 上生成的项目提交一个直接调用 smithy 的 compile 目标,因此在其上工作的其他任何人 - 包括在 macOS 或 Linux 上 - 也需要在他们的 PATH 上有 Smithy CLI。要让这些机器通过 mise 解析 CLI,请将目标切换到 mise 命令,如下面所述。
选择 CLI 的解析方式
Section titled “选择 CLI 的解析方式”macOS 和 Linux 通过 mise 解析 CLI,Windows 使用全局安装的 CLI,但您可以通过编辑模型项目的 project.json 中的 compile 目标的命令在任何平台上选择任一方式。
要使用全局安装的 Smithy CLI 而不是 mise,请将 mise 前缀替换为裸 smithy:
{ "targets": { "compile": { "options": { "commands": ["... npx -y mise@<version> exec smithy@<version> -- smithy build ..."] "commands": ["... smithy build ..."] } } }}要返回到 mise 解析 CLI,请恢复 npx -y mise@<version> exec smithy@<version> -- 前缀。
生成器会自动配置一个 bundle 目标,它使用 Rolldown 来创建部署包:
pnpm nx bundle <project-name>yarn nx bundle <project-name>npx nx bundle <project-name>bunx nx bundle <project-name>Rolldown 配置可以在 rolldown.config.ts 中找到,每个要生成的包都有一个入口。Rolldown 会管理并行创建多个包(如果已定义)。
生成器配置了一个具有热重载功能的本地开发服务器:
pnpm nx serve <backend-project>yarn nx serve <backend-project>npx nx serve <backend-project>bunx nx serve <backend-project>部署您的 Smithy API
Section titled “部署您的 Smithy API”生成器根据您选择的 iac 创建 CDK 或 Terraform 基础设施。
用于部署 API 的 CDK 构造位于 common/constructs 文件夹中:
import { MyApi } from '@my-scope/common-constructs';
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { // Add the API to your stack const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), }); }}这将设置:
- 用于 Smithy 服务的 AWS Lambda 函数
- 作为函数触发器的 API Gateway REST API
- IAM 角色和权限
- CloudWatch 日志组
- X-Ray 跟踪配置
用于部署 API 的 Terraform 模块位于 common/terraform 文件夹中。
API 模块将其 Lambda 部署 zip 暂存在共享的 S3 资产存储桶中 - 有关详细信息,请参阅 Terraform 基础设施指南。每个部署实例化一次 core/asset-bucket 模块,并通过 asset_bucket_name 输入将其 bucket_name 输出传递到每个 API / Lambda 模块:
module "asset_bucket" { source = "../../common/terraform/src/core/asset-bucket"}
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Environment variables for the Lambda function env = { ENVIRONMENT = var.environment LOG_LEVEL = "INFO" }
# Additional IAM policies if needed additional_iam_policy_statements = [ # Add any additional permissions your API needs ]
tags = local.common_tags}这将设置:
- 提供 Smithy API 的 AWS Lambda 函数
- 作为函数触发器的 API Gateway REST API
- IAM 角色和权限
- CloudWatch 日志组
- X-Ray 跟踪配置
- CORS 配置
Terraform 模块提供多个输出:
# Access the API endpointoutput "api_url" { value = module.my_api.stage_invoke_url}
# Access Lambda function detailsoutput "lambda_function_name" { value = module.my_api.lambda_function_name}对于 REST API,生成的构造默认会将 AWS WAFv2 Web ACL 与 API Gateway 阶段关联。Web ACL 使用 AWS 托管的默认规则集(AWSManagedRulesCommonRuleSet 和 AWSManagedRulesKnownBadInputsRuleSet),提供针对常见 Web 漏洞(包括 OWASP Top 10)的保护。WAF 请求日志会写入 CloudWatch Logs 日志组。
您可以编辑生成的 rest-api 构造来添加、删除或调整规则(例如,添加基于速率的规则或其他托管规则组)。
要选择退出(例如,附加您自己的 Web ACL),请将 enableWaf 设置为 false:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), enableWaf: false,});要选择退出(例如,附加您自己的 Web ACL),请将 enable_waf 设置为 false:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name enable_waf = false}访问日志记录
Section titled “访问日志记录”对于 REST API,生成的基础设施默认启用访问日志,为每个请求向专用的 CloudWatch Logs 日志组写入一行结构化的 JSON。该日志组使用客户管理的 KMS 密钥加密,并保留一年。
API Gateway 使用账户级别的 CloudWatch Logs 角色写入访问日志。此角色在 AWS::ApiGateway::Account 设置上配置,该设置是每个区域每个账户的单例 — 区域中的每个 REST API 只有一个角色。为了在多个独立部署的堆栈中安全地管理此角色,生成的基础设施:
- 仅在尚未设置有效角色时创建共享的 CloudWatch Logs 角色并在账户上配置它,因此部署永远不会覆盖另一个堆栈拥有的角色。
- 在拆除时保持账户设置不变,因此销毁一个堆栈永远不会禁用区域中其他 REST API 的日志记录。
账户角色由 ApiGatewayAccount 构造管理,这是一个通过 ApiGatewayAccount.ensure(scope) 解析的堆栈范围单例。每个 REST API 的阶段都依赖于它,该角色由 Lambda 支持的自定义资源配置。
访问日志格式由您的 API 扩展的 RestApi 构造设置。要自定义它,请在生成的 packages/common/constructs/src/app/apis/my-api.ts 中将 deployOptions 传递给 super,同时保留构造已设置的 tracingEnabled:
super(scope, id, { apiName: 'MyApi', // ... deployOptions: { tracingEnabled: true, accessLogFormat: AccessLogFormat.clf(), }, ...props,});AccessLogFormat 从 aws-cdk-lib/aws-apigateway 导入。任何未设置的内容都会保留构造的默认值 — 一个包含标准字段的 JSON 格式。
账户角色由 core/api/api-gateway-account 模块管理,该模块由生成的 API 模块实例化。它以幂等方式配置账户,并且在 terraform destroy 时永远不会重置。
您可以通过编辑生成的 API 模块中 aws_api_gateway_stage 资源上的 access_log_settings 块来自定义访问日志格式。
REST/HTTP API CDK 构造被配置为提供类型安全的接口,用于为每个操作定义集成。
您可以使用静态方法 defaultIntegrations 来使用默认模式,该模式为每个操作定义一个单独的 AWS Lambda 函数:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});Terraform 模块自动使用路由器模式和单个 Lambda 函数。无需额外配置:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# The module automatically creates a single Lambda function # that handles all API operations tags = local.common_tags}您可以通过 API 构造的 integrations 属性以类型安全的方式访问底层的 AWS Lambda 函数。例如,如果您的 API 定义了一个名为 sayHello 的操作,并且您需要向该函数添加一些权限,可以按如下方式操作:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
// sayHello is typed to the operations defined in your APIapi.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: [...], resources: [...],}));如果您的 API 使用 shared 模式,共享的路由器 Lambda 将作为 api.integrations.$router 公开:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
api.integrations.$router.handler.addEnvironment('LOG_LEVEL', 'DEBUG');使用 Terraform 的路由器模式,只有一个 Lambda 函数。您可以通过模块输出访问它:
# Grant additional permissions to the single Lambda functionresource "aws_iam_role_policy" "additional_permissions" { name = "additional-api-permissions" role = module.my_api.lambda_execution_role_name
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:GetObject", "s3:PutObject" ] Resource = "arn:aws:s3:::my-bucket/*" } ] })}自定义默认选项
Section titled “自定义默认选项”如果您想自定义创建每个默认集成的 Lambda 函数时使用的选项,可以使用 withDefaultOptions 方法。例如,如果您希望所有 Lambda 函数都驻留在 Vpc 中:
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});VPC 配置已由生成的模块支持 — 设置 enable_vpc 以及 vpc_id 和 subnet_ids,模块会将 Lambda 函数部署到您的 VPC 中,并为您创建一个安全组:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# VPC configuration enable_vpc = true vpc_id = aws_vpc.main.id subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id]
tags = local.common_tags}对于模块未公开的选项,请直接编辑生成的 Terraform 模块中的 aws_lambda_function 资源。
按操作自定义选项
Section titled “按操作自定义选项”要自定义用于创建_特定_操作的默认集成的选项(而不影响其他操作),可以使用 withOperationOptions 方法。例如,如果您想仅为一个操作增加 Lambda 函数超时时间:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOperationOptions({ sayHello: { timeout: Duration.seconds(60), }, }) .build(),});
// The selected operations remain default integrations, so they're still typed accordingly:api.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ ... }));您指定的选项将与默认集成选项(以及通过 withDefaultOptions 设置的任何选项)合并。请注意,您不能为通过 withOverrides 替换的操作指定选项,因为这些操作不再使用默认集成。
如果同一操作同时被 withOperationOptions 和 withOverrides 针对,无论您调用它们的顺序如何,都会遇到类型错误。
要使用 Terraform 为特定操作自定义选项,您需要编辑生成的 Terraform 模块以配置每个操作的单独 Lambda 函数(请参阅下面的显式集成部分)。
您还可以使用 withOverrides 方法覆盖特定操作的集成。每个覆盖必须指定一个 integration 属性,该属性的类型对应于 HTTP 或 REST API 的适当 CDK 集成构造。withOverrides 方法也是类型安全的。例如,如果您想覆盖 getDocumentation API 以指向某个外部网站托管的文档,可以按如下方式实现:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});您还会注意到,当通过 api.integrations.getDocumentation 访问时,被覆盖的集成不再具有 handler 属性。
您可以向集成添加额外的属性,这些属性也将相应地进行类型化,从而允许其他类型的集成被抽象但保持类型安全。例如,如果您为 REST API 创建了 S3 集成,并且稍后希望引用特定操作的存储桶,可以按如下方式操作:
const storageBucket = new Bucket(this, 'Bucket', { ... });
const apiGatewayRole = new Role(this, 'ApiGatewayS3Role', { assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),});
storageBucket.grantRead(apiGatewayRole);
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getFile: { bucket: storageBucket, integration: new AwsIntegration({ service: 's3', integrationHttpMethod: 'GET', path: `${storageBucket.bucketName}/{fileName}`, options: { credentialsRole: apiGatewayRole, requestParameters: { 'integration.request.path.fileName': 'method.request.querystring.fileName', }, integrationResponses: [{ statusCode: '200' }], }, }), options: { requestParameters: { 'method.request.querystring.fileName': true, }, methodResponses: [{ statusCode: '200', }], } }, }) .build(),});
// Later, perhaps in another file, you can access the bucket property we defined// in a type-safe mannerapi.integrations.getFile.bucket.grantRead(...);您还可以在集成中提供 options 来覆盖特定的方法选项,例如授权器。例如,如果您希望为 getDocumentation 操作使用 Cognito 身份验证:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), options: { authorizer: new CognitoUserPoolsAuthorizer(...) // for REST, or HttpUserPoolAuthorizer for an HTTP API } }, }) .build(),});如果您愿意,可以选择不使用默认集成,而是直接为每个操作提供一个集成。这在以下情况下很有用,例如,每个操作需要使用不同类型的集成,或者您希望在添加新操作时收到类型错误:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});对于使用 Terraform 的显式每个操作集成,您应该修改生成的应用程序特定模块,以用每个操作的特定集成替换默认代理集成。
编辑 packages/common/terraform/src/app/apis/my-api/my-api.tf:
- 删除默认代理路由(例如,
resource "aws_apigatewayv2_route" "proxy_routes") - 用每个操作的单独函数替换单个 Lambda 函数
- 为每个操作创建特定的集成和路由,重用相同的 ZIP 包:
# Remove the default single Lambda function resource "aws_lambda_function" "api_lambda" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApiHandler" role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" runtime = "nodejs22.x" timeout = 30 # ... rest of configuration }
# Remove the default proxy integration resource "aws_apigatewayv2_integration" "lambda_integration" { api_id = module.http_api.api_id integration_type = "AWS_PROXY" integration_uri = aws_lambda_function.api_lambda.invoke_arn # ... rest of configuration }
# Remove the default proxy routes resource "aws_apigatewayv2_route" "proxy_routes" { for_each = toset(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]) api_id = module.http_api.api_id route_key = "${each.key} /{proxy+}" target = "integrations/${aws_apigatewayv2_integration.lambda_integration.id}" # ... rest of configuration }
# Add individual Lambda functions for each operation using the same bundle resource "aws_lambda_function" "say_hello_handler" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApi-SayHello" role = aws_iam_role.lambda_execution_role.arn handler = "sayHello.handler" # Specific handler for this operation runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
resource "aws_lambda_function" "get_documentation_handler" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Specific handler for this operation runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
# Add specific integrations for each operation resource "aws_apigatewayv2_integration" "say_hello_integration" { api_id = module.http_api.api_id integration_type = "AWS_PROXY" integration_uri = aws_lambda_function.say_hello_handler.invoke_arn payload_format_version = "2.0" timeout_milliseconds = 30000 }
resource "aws_apigatewayv2_integration" "get_documentation_integration" { api_id = module.http_api.api_id integration_type = "HTTP_PROXY" integration_uri = "https://example.com/documentation" integration_method = "GET" }
# Add specific routes for each operation resource "aws_apigatewayv2_route" "say_hello_route" { api_id = module.http_api.api_id route_key = "POST /sayHello" target = "integrations/${aws_apigatewayv2_integration.say_hello_integration.id}" authorization_type = "AWS_IAM" }
resource "aws_apigatewayv2_route" "get_documentation_route" { api_id = module.http_api.api_id route_key = "GET /documentation" target = "integrations/${aws_apigatewayv2_integration.get_documentation_integration.id}" authorization_type = "NONE" }
# Add Lambda permissions for each function resource "aws_lambda_permission" "say_hello_permission" { statement_id = "AllowExecutionFromAPIGateway-SayHello" action = "lambda:InvokeFunction" function_name = aws_lambda_function.say_hello_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.http_api.api_execution_arn}/*/*" }
resource "aws_lambda_permission" "get_documentation_permission" { statement_id = "AllowExecutionFromAPIGateway-GetDocumentation" action = "lambda:InvokeFunction" function_name = aws_lambda_function.get_documentation_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.http_api.api_execution_arn}/*/*" }# Remove the default single Lambda function resource "aws_lambda_function" "api_lambda" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApiHandler-${random_string.suffix.result}" role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" runtime = "nodejs22.x" timeout = 30 # ... rest of configuration }
# Remove the default proxy integration resource "aws_api_gateway_integration" "lambda_integration" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.proxy_resource.id http_method = aws_api_gateway_method.proxy_method.http_method integration_http_method = "POST" type = "AWS_PROXY" uri = aws_lambda_function.api_lambda.invoke_arn # ... rest of configuration }
# Remove the default catch-all proxy method resource "aws_api_gateway_method" "proxy_method" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.proxy_resource.id http_method = "ANY" # ... rest of configuration }
# Add individual Lambda functions for each operation using the same bundle resource "aws_lambda_function" "say_hello_handler" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApi-SayHello" role = aws_iam_role.lambda_execution_role.arn handler = "sayHello.handler" # Specific handler for this operation runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
resource "aws_lambda_function" "get_documentation_handler" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Specific handler for this operation runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
# Add specific resources and methods for each operation resource "aws_api_gateway_resource" "say_hello_resource" { rest_api_id = module.rest_api.api_id parent_id = module.rest_api.api_root_resource_id path_part = "sayHello" }
resource "aws_api_gateway_method" "say_hello_method" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.say_hello_resource.id http_method = "POST" authorization = "AWS_IAM" }
resource "aws_api_gateway_integration" "say_hello_integration" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.say_hello_resource.id http_method = aws_api_gateway_method.say_hello_method.http_method
integration_http_method = "POST" type = "AWS_PROXY" uri = aws_lambda_function.say_hello_handler.invoke_arn }
resource "aws_api_gateway_resource" "get_documentation_resource" { rest_api_id = module.rest_api.api_id parent_id = module.rest_api.api_root_resource_id path_part = "documentation" }
resource "aws_api_gateway_method" "get_documentation_method" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.get_documentation_resource.id http_method = "GET" authorization = "NONE" }
resource "aws_api_gateway_integration" "get_documentation_integration" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.get_documentation_resource.id http_method = aws_api_gateway_method.get_documentation_method.http_method
integration_http_method = "GET" type = "HTTP" uri = "https://example.com/documentation" }
# Update deployment to depend on new integrations~ resource "aws_api_gateway_deployment" "api_deployment" { rest_api_id = module.rest_api.api_id
depends_on = [ aws_api_gateway_integration.lambda_integration, aws_api_gateway_integration.say_hello_integration, aws_api_gateway_integration.get_documentation_integration, ]
lifecycle { create_before_destroy = true }
triggers = { redeployment = sha1(jsonencode([ aws_api_gateway_integration.say_hello_integration, aws_api_gateway_integration.get_documentation_integration, ])) } }
# Add Lambda permissions for each function resource "aws_lambda_permission" "say_hello_permission" { statement_id = "AllowExecutionFromAPIGateway-SayHello" action = "lambda:InvokeFunction" function_name = aws_lambda_function.say_hello_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.rest_api.api_execution_arn}/*/*" }
resource "aws_lambda_permission" "get_documentation_permission" { statement_id = "AllowExecutionFromAPIGateway-GetDocumentation" action = "lambda:InvokeFunction" function_name = aws_lambda_function.get_documentation_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.rest_api.api_execution_arn}/*/*" }生成的 CDK API 构造支持两种集成模式:
isolated为每个操作创建一个 Lambda 函数。这是生成的 API 的默认设置。shared创建一个默认路由器 Lambda 并将其重用于每个操作,除非您覆盖特定集成。
isolated 为您提供更细粒度的每个操作权限和配置。shared 减少了 Lambda 和 API Gateway 集成的扩散,同时仍允许选择性覆盖。
例如,将 pattern 设置为 'shared' 会创建一个函数,而不是每个集成一个函数:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { ... return IntegrationBuilder.rest({ pattern: 'shared', ... }); };}Terraform 模块自动使用路由器模式 - 这是默认且唯一支持的方法。生成的模块创建一个处理所有 API 操作的单个 Lambda 函数。
您可以简单地实例化默认模块以获得路由器模式:
# Default router pattern - single Lambda function for all operationsmodule "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Single Lambda function handles all operations automatically tags = local.common_tags}由于操作在 Smithy 中定义,我们使用代码生成向 CDK 构造提供元数据以实现类型安全的集成。
一个 generate:<ApiName>-metadata 目标被添加到通用构造 project.json 中以促进此代码生成,它会发出一个文件,例如 packages/common/constructs/src/generated/my-api/metadata.gen.ts。由于这是在构建时生成的,因此在版本控制中被忽略。
授予访问权限(仅限 IAM)
Section titled “授予访问权限(仅限 IAM)”如果您选择了 IAM 身份验证,您可以使用 grantInvokeAccess 方法授予对 API 的访问权限:
api.grantInvokeAccess(myIdentityPool.authenticatedRole);# Create an IAM policy to allow invoking the APIresource "aws_iam_policy" "api_invoke_policy" { name = "MyApiInvokePolicy" description = "Policy to allow invoking the Smithy API"
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = "execute-api:Invoke" Resource = "${module.my_api.api_execution_arn}/*/*" } ] })}
# Attach the policy to an IAM roleresource "aws_iam_role_policy_attachment" "api_invoke_access" { role = aws_iam_role.authenticated_user_role.name policy_arn = aws_iam_policy.api_invoke_policy.arn}调用您的 Smithy API
Section titled “调用您的 Smithy API”要从 React 网站调用您的 API,您可以使用 connection 生成器,它从您的 Smithy 模型提供类型安全的客户端生成。
使用 connection 生成器将此项目与工作区中的其他项目集成。以下连接涉及此项目: