API TypeScript của Smithy
Smithy là một ngôn ngữ định nghĩa giao diện độc lập với giao thức để tạo API theo cách hướng mô hình.
Trình tạo Smithy TypeScript API tạo một API mới sử dụng Smithy để định nghĩa dịch vụ, và Smithy TypeScript Server SDK để triển khai. Trình tạo cung cấp infrastructure as code bằng CDK hoặc Terraform để triển khai dịch vụ của bạn lên AWS Lambda, được truy cập thông qua AWS API Gateway REST API. Nó cung cấp phát triển API an toàn kiểu với tự động tạo mã từ mô hình Smithy. Handler được tạo sử dụng AWS Lambda Powertools for TypeScript cho khả năng quan sát, bao gồm logging, AWS X-Ray tracing và CloudWatch Metrics
Sử dụng
Phần tiêu đề “Sử dụng”Tạo một Smithy TypeScript API
Phần tiêu đề “Tạo một Smithy TypeScript API”Bạn có thể tạo một Smithy TypeScript API mới theo hai cách:
- Cài đặt Nx Console VSCode Plugin nếu bạn chưa cài đặt
- Mở Nx Console trong VSCode
- Nhấp
Generate (UI)trong phần "Common Nx Commands" - Tìm kiếm
@aws/nx-plugin - ts#api - Điền các tham số bắt buộc
- framework: smithy
- Nhấp
Generate
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=smithyBạn cũng có thể thực hiện chạy thử để xem những tệp nào sẽ bị thay đổi
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-runTùy chọn
Phần tiêu đề “Tùy chọn”| Tham số | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
| name Bắt buộc | string | - | Tên của API (bắt buộc). Được sử dụng để tạo tên class và đường dẫn file. |
| framework | trpc | smithy | trpc | Framework API để sử dụng. |
| namespace | string | - | Namespace cho Smithy API (chỉ áp dụng cho framework smithy). Mặc định là scope của monorepo |
| integrationPattern | isolated | shared | isolated | Cách thức tạo API Gateway integrations cho API. Chọn giữa isolated (mặc định) và shared. |
| auth | iam | cognito | custom | iam | Phương thức được sử dụng để xác thực với API của bạn. Chọn giữa iam (mặc định), cognito hoặc custom. |
| directory | string | packages | Thư mục để lưu trữ ứng dụng. |
| subDirectory | string | - | Thư mục con mà dự án được đặt trong đó. Mặc định đây là tên dự án. |
| iac | inherit | cdk | terraform | inherit | Nhà cung cấp IaC ưa thích. Mặc định được kế thừa từ lựa chọn ban đầu của bạn. |
| infra | rest-lambda | http-lambda | none | rest-lambda | Loại hạ tầng được sử dụng để triển khai API này. |
| preferInstallDependencies | boolean | true | Có nên cài đặt dependencies sau khi generator chạy hay không. Đặt thành false để trì hoãn việc cài đặt khi chạy nhiều generator liên tiếp (việc cài đặt vẫn sẽ chạy nếu cần thiết để các generator tiếp theo có thể tính toán Nx project graph); cài đặt một lần vào cuối. |
Kết quả của Trình tạo
Phần tiêu đề “Kết quả của Trình tạo”Trình tạo tạo hai dự án liên quan trong thư mục <directory>/<api-name>:
Thư mụcmodel/ Dự án mô hình Smithy
- project.json Cấu hình dự án và build targets
- smithy-build.json Cấu hình build Smithy
- build.Dockerfile Cấu hình Docker để build các artifact Smithy
Thư mụcsrc/
- main.smithy Định nghĩa dịch vụ chính
Thư mụcoperations/
- echo.smithy Ví dụ định nghĩa operation
Thư mụcbackend/ Triển khai backend TypeScript
- project.json Cấu hình dự án và build targets
- rolldown.config.ts Cấu hình bundle
Thư mụcsrc/
- handler.ts AWS Lambda handler
- local-server.ts Server phát triển local
- service.ts Triển khai dịch vụ
- context.ts Định nghĩa context dịch vụ
Thư mụcoperations/
- echo.ts Ví dụ triển khai operation
Thư mụcgenerated/ TypeScript SDK được tạo (tạo trong quá trình build)
- …
Infrastructure
Phần tiêu đề “Infrastructure”Vì trình tạo này tạo infrastructure as code dựa trên iacProvider bạn chọn, nó sẽ tạo một dự án trong packages/common bao gồm các CDK constructs hoặc Terraform modules tương ứng.
Dự án infrastructure as code chung được cấu trúc như sau:
Thư mụcpackages/common/constructs
Thư mụcsrc
Thư mụcapp/ Constructs cho infrastructure cụ thể cho một dự án/trình tạo
Thư mụcapis/
- <project-name>.ts CDK construct để triển khai API của bạn
Thư mụccore/ Constructs chung được tái sử dụng bởi constructs trong
appThư mụcapi/
- rest-api.ts CDK construct để triển khai REST API
- utils.ts Tiện ích cho API constructs
- index.ts Entry point xuất các constructs từ
app
- project.json Build targets và cấu hình dự án
Thư mụcpackages/common/terraform
Thư mụcsrc
Thư mụcapp/ Terraform modules cho infrastructure cụ thể cho một dự án/trình tạo
Thư mụcapis/
Thư mục<project-name>/
- <project-name>.tf Module để triển khai API của bạn
Thư mụccore/ Modules chung được tái sử dụng bởi modules trong
appThư mụcapi/
Thư mụcrest-api/
- rest-api.tf Module để triển khai REST API
- project.json Build targets và cấu hình dự án
Kiến trúc
Phần tiêu đề “Kiến trúc”Smithy API được triển khai có kiến trúc như sau, với AWS WAFv2 Web ACL đặt trước API Gateway stage:
Triển khai Smithy API của bạn
Phần tiêu đề “Triển khai Smithy API của bạn”Định nghĩa Operations trong Smithy
Phần tiêu đề “Định nghĩa Operations trong Smithy”Operations được định nghĩa trong các file Smithy trong dự án model. Định nghĩa dịch vụ chính nằm trong 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, // Thêm operations của bạn ở đây ] errors: [ ValidationException ]}Các operations riêng lẻ được định nghĩa trong các file riêng biệt trong thư mục 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}Triển khai Operations trong TypeScript
Phần tiêu đề “Triển khai Operations trong TypeScript”Các triển khai operation nằm trong thư mục src/operations/ của dự án backend. Mỗi operation được triển khai sử dụng các kiểu được tạo từ TypeScript Server SDK (được tạo tại thời điểm build từ mô hình Smithy của bạn).
import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input) => { // Logic nghiệp vụ của bạn ở đây return { message: `Echo: ${input.message}` // an toàn kiểu dựa trên mô hình Smithy của bạn };};Operations phải được đăng ký vào định nghĩa dịch vụ trong src/service.ts:
import { ServiceContext } from './context.js';import { YourServiceService } from './generated/ssdk/index.js';import { Echo } from './operations/echo.js';// Import các operations khác ở đây
// Đăng ký operations vào dịch vụ ở đâyexport const Service: YourServiceService<ServiceContext> = { Echo, // Thêm các operations khác ở đây};Service Context
Phần tiêu đề “Service Context”Bạn có thể định nghĩa context được chia sẻ cho các operations của bạn trong context.ts:
export interface ServiceContext { // Powertools tracer, logger và metrics được cung cấp mặc định tracer: Tracer; logger: Logger; metrics: Metrics; // Thêm các dependencies được chia sẻ, kết nối database, v.v. dbClient: any; userIdentity: string;}Context này được truyền cho tất cả các triển khai operation và có thể được sử dụng để chia sẻ tài nguyên như kết nối database, cấu hình, hoặc tiện ích logging.
Khả năng quan sát với AWS Lambda Powertools
Phần tiêu đề “Khả năng quan sát với AWS Lambda Powertools”Logging
Phần tiêu đề “Logging”Trình tạo cấu hình structured logging sử dụng AWS Lambda Powertools với tự động inject context thông qua Middy middleware.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Bạn có thể tham chiếu logger từ các triển khai operation của bạn thông qua context:
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('Thông điệp log của bạn'); // ...};Tracing
Phần tiêu đề “Tracing”AWS X-Ray tracing được cấu hình tự động thông qua middleware captureLambdaHandler.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Bạn có thể thêm subsegments tùy chỉnh vào traces của bạn trong các operations:
import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => { // Tạo một subsegment mới const subsegment = ctx.tracer.getSegment()?.addNewSubsegment('custom-operation'); try { // Logic của bạn ở đây } catch (error) { subsegment?.addError(error as Error); throw error; } finally { subsegment?.close(); }};Metrics
Phần tiêu đề “Metrics”CloudWatch metrics được thu thập tự động cho mỗi request thông qua middleware logMetrics.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Bạn có thể thêm custom metrics trong các operations của bạn:
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); // ...};Xử lý Lỗi
Phần tiêu đề “Xử lý Lỗi”Smithy cung cấp xử lý lỗi tích hợp sẵn. Bạn có thể định nghĩa các lỗi tùy chỉnh trong mô hình Smithy của bạn:
@error("client")@httpError(400)structure InvalidRequestError { @required message: String}Và đăng ký chúng vào operation/service của bạn:
operation MyOperation { ... errors: [InvalidRequestError]}Sau đó ném chúng trong triển khai TypeScript của bạn:
import { InvalidRequestError } from '../generated/ssdk/index.js';
export const MyOperation: MyOperationHandler<ServiceContext> = async (input) => { if (!input.requiredField) { throw new InvalidRequestError({ message: "Trường bắt buộc bị thiếu" }); }
return { /* phản hồi thành công */ };};Truy Cập Người Dùng Đang Gọi
Phần tiêu đề “Truy Cập Người Dùng Đang Gọi”Khi API của bạn được bảo vệ bằng xác thực, các operation thường cần biết ai đang gọi. Cách tiếp cận được khuyến nghị là giải quyết danh tính của người gọi một lần trong handler và truyền nó qua service context để các operation cụ thể sử dụng.
Chúng ta sẽ mô hình hóa trường hợp không được ủy quyền như một lỗi Smithy để nó được tuần tự hóa thành phản hồi 403 phù hợp. Thêm nó vào mô hình của bạn, ví dụ trong model/src/operations/errors.smithy, và tham chiếu nó trên bất kỳ operation nào yêu cầu danh tính:
$version: "2.0"
namespace your.namespace
/// Thrown when the calling user cannot be determined@error("client")@httpError(403)structure UnauthorizedError { @required message: String}Đầu tiên, hiển thị danh tính đã giải quyết trên service context trong src/context.ts. Chúng ta cung cấp nó dưới dạng một hàm để UnauthorizedError được ném ra từ bên trong một operation (nơi Server SDK tuần tự hóa nó thành 403), thay vì từ handler:
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>;}Tiếp theo, viết resolver trong src/identity.ts. Nó ném UnauthorizedError khi không thể xác định người gọi. Cách triển khai phụ thuộc vào phương thức auth đã chọn:
Đối với xác thực IAM, chúng ta tra cứu người gọi trong Cognito bằng cách sử dụng sub được trích xuất từ sự kiện API Gateway:
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! };};Với auth: 'cognito', trình ủy quyền Cognito User Pools của API Gateway xác minh JWT mà người gọi cung cấp trong header Authorization và đặt các claim đã xác minh trên sự kiện tại 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 };};Sau đó kết nối resolver vào context trong 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),});Bây giờ chúng ta có thể sử dụng danh tính đã giải quyết trong một operation, ví dụ trong 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}` };};Building và Tạo Mã
Phần tiêu đề “Building và Tạo Mã”Dự án mô hình Smithy sử dụng Docker để build các artifact Smithy và tạo TypeScript Server SDK:
pnpm nx build <model-project>yarn nx build <model-project>npx nx build <model-project>bunx nx build <model-project>Quá trình này:
- Biên dịch mô hình Smithy và xác thực nó
- Tạo đặc tả OpenAPI từ mô hình Smithy
- Tạo TypeScript Server SDK với các giao diện operation an toàn kiểu
- Xuất các build artifacts ra
dist/<model-project>/build/
Dự án backend tự động sao chép SDK được tạo trong quá trình biên dịch:
pnpm nx copy-ssdk <backend-project>yarn nx copy-ssdk <backend-project>npx nx copy-ssdk <backend-project>bunx nx copy-ssdk <backend-project>Bundle Target
Phần tiêu đề “Bundle Target”Generator tự động cấu hình một target bundle sử dụng Rolldown để tạo gói triển khai:
pnpm nx bundle <project-name>yarn nx bundle <project-name>npx nx bundle <project-name>bunx nx bundle <project-name>Cấu hình Rolldown có thể được tìm thấy trong rolldown.config.ts, với một entry cho mỗi bundle cần tạo. Rolldown quản lý việc tạo nhiều bundle song song nếu được định nghĩa.
Phát triển Local
Phần tiêu đề “Phát triển Local”Trình tạo cấu hình một server phát triển local với hot reloading:
pnpm nx serve <backend-project>yarn nx serve <backend-project>npx nx serve <backend-project>bunx nx serve <backend-project>Triển khai Smithy API của bạn
Phần tiêu đề “Triển khai Smithy API của bạn”Trình tạo tạo infrastructure CDK hoặc Terraform dựa trên iacProvider bạn đã chọn.
CDK construct để triển khai API của bạn nằm trong thư mục common/constructs:
import { MyApi } from ':my-scope/common-constructs';
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { // Thêm API vào stack của bạn const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), }); }}Điều này thiết lập:
- Một AWS Lambda function cho dịch vụ Smithy
- API Gateway REST API làm trigger cho function
- IAM roles và permissions
- CloudWatch log group
- Cấu hình X-Ray tracing
Các Terraform modules để triển khai API của bạn nằm trong thư mục common/terraform.
API module lưu trữ tạm thời file zip triển khai Lambda trong một S3 asset bucket được chia sẻ — xem hướng dẫn infrastructure Terraform để biết chi tiết. Khởi tạo module core/asset-bucket một lần cho mỗi deployment và truyền output bucket_name của nó vào mọi API / Lambda module thông qua input asset_bucket_name:
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
# Biến môi trường cho Lambda function env = { ENVIRONMENT = var.environment LOG_LEVEL = "INFO" }
# Các IAM policies bổ sung nếu cần additional_iam_policy_statements = [ # Thêm bất kỳ quyền bổ sung nào mà API của bạn cần ]
tags = local.common_tags}Điều này thiết lập:
- Một AWS Lambda function phục vụ Smithy API
- API Gateway REST API làm trigger cho function
- IAM roles và permissions
- CloudWatch log group
- Cấu hình X-Ray tracing
- Cấu hình CORS
Terraform module cung cấp một số outputs:
# Truy cập API endpointoutput "api_url" { value = module.my_api.stage_invoke_url}
# Truy cập chi tiết Lambda functionoutput "lambda_function_name" { value = module.my_api.lambda_function_name}Đối với REST API, construct được tạo ra sẽ liên kết một AWS WAFv2 Web ACL với API Gateway stage theo mặc định. Web ACL sử dụng bộ quy tắc mặc định được quản lý bởi AWS (AWSManagedRulesCommonRuleSet và AWSManagedRulesKnownBadInputsRuleSet), cung cấp bảo vệ chống lại các khai thác web phổ biến bao gồm OWASP Top 10. Nhật ký yêu cầu WAF được ghi vào nhóm CloudWatch Logs.
Bạn có thể chỉnh sửa construct rest-api được tạo ra để thêm, xóa hoặc điều chỉnh các quy tắc (ví dụ: để thêm quy tắc dựa trên tốc độ hoặc các nhóm quy tắc được quản lý bổ sung).
Để từ chối (ví dụ: để đính kèm Web ACL của riêng bạn), đặt enableWaf thành false:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), enableWaf: false,});Để từ chối (ví dụ: để đính kèm Web ACL của riêng bạn), đặt enable_waf thành false:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name enable_waf = false}Ghi nhật ký truy cập
Phần tiêu đề “Ghi nhật ký truy cập”Đối với REST API, cơ sở hạ tầng được tạo ra sẽ bật ghi nhật ký truy cập theo mặc định, ghi một dòng JSON có cấu trúc cho mỗi yêu cầu vào một nhóm CloudWatch Logs chuyên dụng. Nhóm nhật ký được mã hóa bằng khóa KMS do khách hàng quản lý và được giữ lại trong một năm.
API Gateway ghi nhật ký truy cập bằng cách sử dụng vai trò CloudWatch Logs ở cấp tài khoản. Vai trò này được cấu hình trên cài đặt AWS::ApiGateway::Account, đây là một singleton cho mỗi vùng trên mỗi tài khoản — chỉ có một vai trò duy nhất cho mọi REST API trong vùng. Để quản lý điều này một cách an toàn trên nhiều stack được triển khai độc lập, cơ sở hạ tầng được tạo ra sẽ:
- Tạo một vai trò CloudWatch Logs được chia sẻ và cấu hình nó trên tài khoản chỉ khi chưa có vai trò hoạt động nào được thiết lập, do đó các triển khai không bao giờ ghi đè vai trò mà một stack khác sở hữu.
- Giữ nguyên cài đặt tài khoản khi dỡ bỏ, do đó việc hủy một stack không bao giờ vô hiệu hóa ghi nhật ký cho các REST API khác trong vùng.
Vai trò tài khoản được quản lý bởi construct ApiGatewayAccount, một singleton có phạm vi stack được giải quyết thông qua ApiGatewayAccount.ensure(scope). Mỗi stage của REST API phụ thuộc vào nó, và vai trò được cấu hình bởi một tài nguyên tùy chỉnh được hỗ trợ bởi Lambda.
Bạn có thể tùy chỉnh định dạng nhật ký truy cập bằng cách truyền deployOptions khi xây dựng API của bạn:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), deployOptions: { accessLogFormat: AccessLogFormat.clf(), },});Vai trò tài khoản được quản lý bởi module core/api/api-gateway-account, được khởi tạo bởi module API được tạo ra. Nó cấu hình tài khoản một cách idempotent và không bao giờ được đặt lại khi chạy terraform destroy.
Bạn có thể tùy chỉnh định dạng nhật ký truy cập bằng cách chỉnh sửa khối access_log_settings trên tài nguyên aws_api_gateway_stage trong module API được tạo ra.
Integrations
Phần tiêu đề “Integrations”Các construct CDK của REST/HTTP API được cấu hình để cung cấp giao diện type-safe cho việc định nghĩa các tích hợp cho từng operation của bạn.
Tích hợp mặc định
Phần tiêu đề “Tích hợp mặc định”Bạn có thể sử dụng defaultIntegrations tĩnh để sử dụng pattern mặc định, định nghĩa một AWS Lambda function riêng biệt cho mỗi operation:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});Các module Terraform tự động sử dụng router pattern với một Lambda function duy nhất. Không cần cấu hình thêm:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Module tự động tạo một Lambda function duy nhất # xử lý tất cả các operation API tags = local.common_tags}Truy cập các tích hợp
Phần tiêu đề “Truy cập các tích hợp”Bạn có thể truy cập các AWS Lambda function bên dưới thông qua thuộc tính integrations của construct API, theo cách type-safe. Ví dụ, nếu API của bạn định nghĩa một operation tên là sayHello và bạn cần thêm một số quyền cho function này, bạn có thể làm như sau:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
// sayHello được type theo các operation được định nghĩa trong API của bạnapi.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: [...], resources: [...],}));Nếu API của bạn sử dụng pattern shared, router Lambda dùng chung được expose dưới dạng api.integrations.$router:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
api.integrations.$router.handler.addEnvironment('LOG_LEVEL', 'DEBUG');Với router pattern của Terraform, chỉ có một Lambda function duy nhất. Bạn có thể truy cập nó thông qua các output của module:
# Cấp thêm quyền cho Lambda function duy nhấtresource "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/*" } ] })}Tùy chỉnh các tùy chọn mặc định
Phần tiêu đề “Tùy chỉnh các tùy chọn mặc định”Nếu bạn muốn tùy chỉnh các tùy chọn được sử dụng khi tạo Lambda function cho mỗi tích hợp mặc định, bạn có thể sử dụng phương thức withDefaultOptions. Ví dụ, nếu bạn muốn tất cả các Lambda function của mình nằm trong một Vpc:
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});Để tùy chỉnh các tùy chọn như cấu hình VPC, bạn cần chỉnh sửa module Terraform đã được tạo. Ví dụ, để thêm hỗ trợ VPC cho tất cả các Lambda function:
# Thêm các biến VPCvariable "vpc_subnet_ids" { description = "List of VPC subnet IDs for Lambda function" type = list(string) default = []}
variable "vpc_security_group_ids" { description = "List of VPC security group IDs for Lambda function" type = list(string) default = []}
# Cập nhật resource Lambda functionresource "aws_lambda_function" "api_lambda" { # ... cấu hình hiện có ...
# Thêm cấu hình VPC vpc_config { subnet_ids = var.vpc_subnet_ids security_group_ids = var.vpc_security_group_ids }}Sau đó sử dụng module với cấu hình VPC:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Cấu hình VPC vpc_subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id] vpc_security_group_ids = [aws_security_group.lambda_sg.id]
tags = local.common_tags}Tùy chỉnh các tùy chọn cho từng operation
Phần tiêu đề “Tùy chỉnh các tùy chọn cho từng operation”Để tùy chỉnh các tùy chọn được sử dụng để tạo tích hợp mặc định cho các operation cụ thể (mà không ảnh hưởng đến các operation khác), bạn có thể sử dụng phương thức withOperationOptions. Ví dụ, nếu bạn muốn tăng thời gian chờ của Lambda function chỉ cho một operation:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOperationOptions({ sayHello: { timeout: Duration.seconds(60), }, }) .build(),});
// Các operation được chọn vẫn là tích hợp mặc định, vì vậy chúng vẫn được type tương ứng:api.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ ... }));Các tùy chọn bạn chỉ định được hợp nhất với các tùy chọn tích hợp mặc định (và bất kỳ tùy chọn nào được đặt thông qua withDefaultOptions). Lưu ý rằng bạn không thể chỉ định tùy chọn cho các operation mà bạn đã thay thế thông qua withOverrides, vì chúng không còn sử dụng tích hợp mặc định nữa.
Bạn sẽ gặp lỗi type nếu cùng một operation được nhắm mục tiêu bởi cả withOperationOptions và withOverrides, bất kể thứ tự bạn gọi chúng.
Để tùy chỉnh các tùy chọn cho các operation cụ thể với Terraform, bạn cần chỉnh sửa module Terraform đã được tạo để cấu hình các Lambda function riêng lẻ cho từng operation (xem phần Tích hợp rõ ràng bên dưới).
Ghi đè các tích hợp
Phần tiêu đề “Ghi đè các tích hợp”Bạn cũng có thể ghi đè các tích hợp cho các operation cụ thể bằng phương thức withOverrides. Mỗi ghi đè phải chỉ định một thuộc tính integration được type theo construct tích hợp CDK phù hợp cho HTTP hoặc REST API. Phương thức withOverrides cũng là type-safe. Ví dụ, nếu bạn muốn ghi đè một API getDocumentation để trỏ đến tài liệu được lưu trữ bởi một trang web bên ngoài, bạn có thể thực hiện như sau:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});Bạn cũng sẽ nhận thấy rằng tích hợp được ghi đè không còn có thuộc tính handler khi truy cập nó thông qua api.integrations.getDocumentation.
Bạn có thể thêm các thuộc tính bổ sung vào một tích hợp cũng sẽ được type tương ứng, cho phép các loại tích hợp khác được trừu tượng hóa nhưng vẫn giữ type-safe, ví dụ nếu bạn đã tạo một tích hợp S3 cho REST API và sau này muốn tham chiếu bucket cho một operation cụ thể, bạn có thể làm như sau:
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(),});
// Sau này, có thể trong một file khác, bạn có thể truy cập thuộc tính bucket mà chúng ta đã định nghĩa// theo cách type-safeapi.integrations.getFile.bucket.grantRead(...);Ghi đè các Authorizer
Phần tiêu đề “Ghi đè các Authorizer”Bạn cũng có thể cung cấp options trong tích hợp của mình để ghi đè các tùy chọn method cụ thể như authorizer, ví dụ nếu bạn muốn sử dụng xác thực Cognito cho operation getDocumentation của mình:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), options: { authorizer: new CognitoUserPoolsAuthorizer(...) // cho REST, hoặc HttpUserPoolAuthorizer cho HTTP API } }, }) .build(),});Tích hợp rõ ràng
Phần tiêu đề “Tích hợp rõ ràng”Nếu bạn muốn, bạn có thể chọn không sử dụng các tích hợp mặc định và thay vào đó cung cấp trực tiếp một tích hợp cho mỗi operation. Điều này hữu ích nếu, ví dụ, mỗi operation cần sử dụng một loại tích hợp khác nhau hoặc bạn muốn nhận lỗi type khi thêm các operation mới:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});Để có các tích hợp rõ ràng cho từng operation với Terraform, bạn nên sửa đổi module dành riêng cho ứng dụng đã được tạo để thay thế tích hợp proxy mặc định bằng các tích hợp cụ thể cho từng operation.
Chỉnh sửa packages/common/terraform/src/app/apis/my-api/my-api.tf:
- Xóa các route proxy mặc định (ví dụ:
resource "aws_apigatewayv2_route" "proxy_routes") - Thay thế Lambda function duy nhất bằng các function riêng lẻ cho từng operation
- Tạo các tích hợp và route cụ thể cho từng operation, tái sử dụng cùng một bundle ZIP:
# Xóa Lambda function mặc định duy nhất resource "aws_lambda_function" "api_lambda" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApiHandler" role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" runtime = "nodejs22.x" timeout = 30 # ... phần còn lại của cấu hình }
# Xóa tích hợp proxy mặc định 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 # ... phần còn lại của cấu hình }
# Xóa các route proxy mặc định 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}" # ... phần còn lại của cấu hình }
# Thêm các Lambda function riêng lẻ cho mỗi operation sử dụng cùng một bundle resource "aws_lambda_function" "say_hello_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-SayHello" role = aws_iam_role.lambda_execution_role.arn handler = "sayHello.handler" # Handler cụ thể cho operation này 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" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Handler cụ thể cho operation này 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 }
# Thêm các tích hợp cụ thể cho mỗi 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" }
# Thêm các route cụ thể cho mỗi 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" }
# Thêm quyền Lambda cho mỗi 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}/*/*" }# Xóa Lambda function mặc định duy nhất resource "aws_lambda_function" "api_lambda" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApiHandler" role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" runtime = "nodejs22.x" timeout = 30 # ... phần còn lại của cấu hình }
# Xóa tích hợp proxy mặc định 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 # ... phần còn lại của cấu hình }
# Xóa các route proxy mặc định 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}" # ... phần còn lại của cấu hình }
# Thêm các Lambda function riêng lẻ cho mỗi operation sử dụng cùng một bundle resource "aws_lambda_function" "say_hello_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-SayHello" role = aws_iam_role.lambda_execution_role.arn handler = "sayHello.handler" # Handler cụ thể cho operation này 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" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Handler cụ thể cho operation này 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 }
# Thêm các resource và method cụ thể cho mỗi 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" }
# Cập nhật deployment để phụ thuộc vào các tích hợp mới~ 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, ])) } }
# Thêm quyền Lambda cho mỗi 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}/*/*" }Integration Pattern
Phần tiêu đề “Integration Pattern”Các construct CDK API được tạo hỗ trợ hai integration pattern:
isolatedtạo một Lambda function cho mỗi operation. Đây là mặc định cho các API được tạo.sharedtạo một router Lambda mặc định duy nhất và tái sử dụng nó cho mọi operation trừ khi bạn ghi đè các tích hợp cụ thể.
isolated cung cấp cho bạn quyền và cấu hình chi tiết hơn cho mỗi operation. shared giảm sự phân tán của Lambda và tích hợp API Gateway trong khi vẫn cho phép ghi đè có chọn lọc.
Ví dụ, đặt pattern thành 'shared' sẽ tạo một function duy nhất thay vì một function cho mỗi tích hợp:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { ... return IntegrationBuilder.rest({ pattern: 'shared', ... }); };}Các module Terraform tự động sử dụng router pattern - đây là cách tiếp cận mặc định và duy nhất được hỗ trợ. Module được tạo tạo một Lambda function duy nhất xử lý tất cả các operation API.
Bạn chỉ cần khởi tạo module mặc định để có router pattern:
# Router pattern mặc định - Lambda function duy nhất cho tất cả các operationmodule "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Lambda function duy nhất xử lý tất cả các operation tự động tags = local.common_tags}Tạo Mã
Phần tiêu đề “Tạo Mã”Vì operations được định nghĩa trong Smithy, chúng tôi sử dụng tạo mã để cung cấp metadata cho CDK construct để có integrations an toàn kiểu.
Một target generate:<ApiName>-metadata được thêm vào project.json của common constructs để tạo điều kiện cho việc tạo mã này, xuất một file như packages/common/constructs/src/generated/my-api/metadata.gen.ts. Vì điều này được tạo tại thời điểm build, nó bị bỏ qua trong kiểm soát phiên bản.
Cấp Quyền Truy cập (Chỉ IAM)
Phần tiêu đề “Cấp Quyền Truy cập (Chỉ IAM)”Nếu bạn đã chọn xác thực IAM, bạn có thể sử dụng phương thức grantInvokeAccess để cấp quyền truy cập vào API của bạn:
api.grantInvokeAccess(myIdentityPool.authenticatedRole);# Tạo một IAM policy để cho phép gọi APIresource "aws_iam_policy" "api_invoke_policy" { name = "MyApiInvokePolicy" description = "Policy để cho phép gọi Smithy API"
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = "execute-api:Invoke" Resource = "${module.my_api.api_execution_arn}/*/*" } ] })}
# Đính kèm policy vào 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}Gọi Smithy API của bạn
Phần tiêu đề “Gọi Smithy API của bạn”Để gọi API của bạn từ một React website, bạn có thể sử dụng trình tạo connection, cung cấp tạo client an toàn kiểu từ mô hình Smithy của bạn.
Kết nối
Phần tiêu đề “Kết nối”Sử dụng generator connection để tích hợp dự án này với các dự án khác trong workspace của bạn. Các kết nối sau liên quan đến dự án này: