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:
运行此生成器@aws/nx-plugin:ts#api
pnpm nx g @aws/nx-plugin:ts#api yarn nx g @aws/nx-plugin:ts#api npx nx g @aws/nx-plugin:ts#api bunx nx g @aws/nx-plugin:ts#api- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - ts#api - 填写必需参数
- 点击
Generate
构建你的命令10
必需
framework = smithy
name必需stringAPI 的名称(必填)。用于生成类名和文件路径。
frameworkenum默认值:trpc要使用的API框架。
trpcsmithyintegrationPatternenum默认值:isolatedAPI Gateway 集成的生成方式。可选择 isolated(默认)或 shared。
isolatedsharedauthenum默认值:iam用于对 API 进行身份验证的方法。可选择 iam(默认)、cognito 或 custom。
iamcognitocustomdirectorystring默认值:packages存储应用程序的目录。
iacenum默认值:inherit首选的 IaC 提供商。默认情况下,这将继承您的初始选择。
inheritcdkterraforminfraenum默认值:rest-lambda用于部署此 API 的基础设施类型。
rest-lambdanonenamespacestringframework = smithySmithy API 的命名空间(仅适用于 smithy 框架)。默认为您的 monorepo 作用域
subDirectorystring项目所在的子目录。默认情况下为项目名称。
preferInstallDependenciesboolean默认值:true是否在生成器运行后优先安装依赖项。设置为 false 可在批量运行多个生成器时延迟安装(如果后续生成器需要计算 Nx 项目图,仍会运行安装);在最后统一安装一次。
生成器在 <directory>/<api-name> 目录中创建两个相关项目:
文件夹model/ Smithy 模型项目
- project.json 项目配置和构建目标
- smithy-build.json Smithy 构建配置
- ssdk.rolldown.config.mjs 打包生成的 TypeScript Server SDK
文件夹src/
- main.smithy 主服务定义
文件夹operations/
- echo.smithy 示例操作定义
文件夹backend/ TypeScript 后端实现
- package.json 定义项目包名称和依赖项的项目清单
- project.json 项目配置和构建目标
- rolldown.config.ts 打包配置
- tsconfig.json TypeScript 配置
- tsconfig.lib.json TypeScript 配置(用于库源代码)
- tsconfig.spec.json TypeScript 配置(用于测试)
- vitest.config.mts Vitest 配置
文件夹src/
- index.ts 包入口点
- 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 生成器生成一个:
运行此生成器@aws/nx-plugin:smithy#project
pnpm nx g @aws/nx-plugin:smithy#project yarn nx g @aws/nx-plugin:smithy#project npx nx g @aws/nx-plugin:smithy#project bunx nx g @aws/nx-plugin:smithy#project- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - smithy#project - 填写必需参数
- 点击
Generate
构建你的命令7
必需
然后,您的 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 中查找调用者。查找使用 Cognito Identity Provider 客户端,它不是生成的 Smithy 后端的依赖项,因此首先将其安装到后端项目中:
pnpm add @aws-sdk/client-cognito-identity-provider@3.1126.0 --filter my-apiyarn workspace @my-scope/my-api add @aws-sdk/client-cognito-identity-provider@3.1126.0npm install --legacy-peer-deps @aws-sdk/client-cognito-identity-provider@3.1126.0 -w packages/my-api/backendbun add @aws-sdk/client-cognito-identity-provider@3.1126.0 --cwd packages/my-api/backendimport { 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),});getIdentity 是 ServiceContext 上的必需字段,正如上面的注意事项所述,上下文在两个入口点中构造 - 因此 src/local-server.ts 也需要它。本地服务器前面没有 API Gateway 授权器,因此为本地开发提供一个存根身份:
const httpResponse = await serviceHandler.handle(httpRequest, { tracer, logger, metrics, getIdentity: async () => ({ sub: 'local', username: 'local' }),});我们现在可以在操作中使用已解析的身份,例如在 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(),});生成的模块已经为生成 API 时使用的模式定义了默认集成,因此不需要额外的配置:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
tags = local.common_tags}使用默认的 isolated 模式,这会为每个操作创建一个 Lambda 函数。
您可以通过 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');请注意,如果您通过 withOverrides 覆盖了每个操作,$router 将不再可用,因为没有操作继续使用默认路由器集成。
使用 isolated 模式,模块的输出是以操作名称为键的映射,因此您可以访问单个操作的资源。例如,要为一个操作的 Lambda 函数授予额外权限:
# Grant additional permissions to just the sayHello operation's functionresource "aws_iam_role_policy" "say_hello_permissions" { name = "say-hello-additional-permissions" role = module.my_api.lambda_execution_role_names["sayHello"]
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:GetObject", "s3:PutObject" ] Resource = "arn:aws:s3:::my-bucket/*" } ] })}要为每个操作授予相同的权限,可以迭代 operations 输出:
resource "aws_iam_role_policy" "additional_permissions" { for_each = toset(module.my_api.operations)
name = "additional-api-permissions" role = module.my_api.lambda_execution_role_names[each.key]
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = ["s3:GetObject"] Resource = "arn:aws:s3:::my-bucket/*" } ] })}模块还公开了 lambda_function_names、lambda_function_arns、lambda_invoke_arns、integration_ids 和 lambda_log_group_names 作为以操作名称为键的映射。使用 shared 模式时,会公开等效的单数输出(lambda_execution_role_name、lambda_function_name、…),因为只有一个函数。
每个操作都需要的权限最好传递给模块,模块会将它们应用到每个函数的角色:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
additional_iam_policy_statements = [ { Effect = "Allow" Action = ["s3:GetObject"] 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 资源。使用 isolated 模式时,该单个资源声明为 for_each = local.operations,因此在那里进行的编辑会应用于每个操作。
按操作自定义选项
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 针对,无论您调用它们的顺序如何,都会遇到类型错误。
使用 isolated 模式时,Lambda 函数资源已经是按操作的,因此可以按操作名称变化选项。例如,要为一个操作提供更长的超时时间,请编辑生成的模块中的 aws_lambda_function 资源:
resource "aws_lambda_function" "api_lambda" { for_each = local.operations
# Default to 30 seconds, but allow longer for specific operations timeout = lookup({ sayHello = 60 }, each.key, 30)
# ... rest of configuration}您还可以使用 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(...);要将特定操作指向不同的集成类型,请将其从默认的 for_each 中排除,并单独声明其集成。例如,要从外部网站提供 getDocumentation:
# Exclude the overridden operation from the default per-operation resourceslocals { overridden_operations = ["getDocumentation"] default_operations = { for op, details in local.operations : op => details if !contains(local.overridden_operations, op) }}
# Then use local.default_operations in place of local.operations for the# aws_lambda_function, aws_iam_role, aws_apigatewayv2_integration and# aws_lambda_permission resources, and add the override:resource "aws_apigatewayv2_integration" "get_documentation" { api_id = module.http_api.api_id integration_type = "HTTP_PROXY" integration_uri = "https://example.com/documentation" integration_method = "GET"}
resource "aws_apigatewayv2_route" "get_documentation" { api_id = module.http_api.api_id route_key = local.route_key["getDocumentation"] target = "integrations/${aws_apigatewayv2_integration.get_documentation.id}"}您还可以在集成中提供 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(),});授权在每个操作的路由(HTTP API)或方法(REST API)上设置,因此可以按操作名称变化。例如,要在 HTTP API 上保留一个操作未经身份验证:
resource "aws_apigatewayv2_route" "operation_routes" { for_each = local.operations
# ... rest of configuration
authorization_type = each.key == "getDocumentation" ? "NONE" : "AWS_IAM"}对于 IAM 身份验证的 REST API,还要添加资源策略语句,允许未经身份验证访问该操作的路径。
如果您愿意,可以选择不使用默认集成,而是直接为每个操作提供一个集成。这在以下情况下很有用,例如,每个操作需要使用不同类型的集成,或者您希望在添加新操作时收到类型错误:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});将 isolated 模式使用的 for_each 替换为每个操作的 Lambda 函数、集成和权限的显式实例化。
生成的 API 支持两种集成模式:
isolated为每个操作创建一个 Lambda 函数。这是 API 的默认和推荐选项。shared创建一个默认路由器 Lambda 并将其重用于每个操作,除非您覆盖特定集成。
isolated 为您提供更细粒度的每个操作权限和配置,以及更好的日志和跟踪分离。shared 减少了低使用率 API 遇到冷启动的可能性。
集成模式可以随时在 CDK 中通过更新 API 构造来更改。例如,将 pattern 设置为 'shared' 会创建一个函数,而不是每个集成一个函数:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { ... return IntegrationBuilder.rest({ pattern: 'shared', ... }); };}与 CDK 不同,集成模式被烘焙到生成的模块中。要更改集成模式:
- 删除
packages/common/terraform/src/app/apis中先前生成的 API 模块 - 使用其他集成模式重新运行创建 API 的生成器(例如
--integrationPattern=shared)
使用 isolated 模式时,模块从生成的文件中读取操作:
locals { operations_file = "${path.module}/../../../generated/my-api/operations.json" operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}}此文件是从您的 API 生成的,因此您无需手动编辑它。在 API 应用程序代码中添加操作会在下次部署时添加路由和 lambda 函数。默认情况下它被 .gitignore;如果您希望将其签入,请删除该条目。
Terraform REST API 路径深度限制
Section titled “Terraform REST API 路径深度限制”由于操作在 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 生成器将此项目与工作区中的其他项目集成。以下连接涉及此项目: