Smithy TypeScript API
Smithyは、モデル駆動方式でAPIを作成するためのプロトコル非依存のインターフェース定義言語です。
Smithy TypeScript APIジェネレーターは、サービス定義にSmithyを使用し、実装にSmithy TypeScript Server SDKを使用して新しいAPIを作成します。このジェネレーターは、AWS API Gateway REST API経由で公開されるAWS LambdaにサービスをデプロイするためのCDKまたはTerraformのインフラストラクチャコードを提供します。Smithyモデルからの自動コード生成により、型安全なAPI開発を実現します。生成されたハンドラーは、ロギング、AWS X-Rayトレーシング、CloudWatch Metricsを含む可観測性のためにAWS Lambda Powertools for TypeScriptを使用します。
Smithy TypeScript APIの生成
Section titled “Smithy TypeScript APIの生成”新しいSmithy TypeScript APIは2つの方法で生成できます:
このジェネレーターを実行@aws/nx-plugin:ts#api
pnpm nx g @aws/nx-plugin:ts#api --framework=smithy yarn nx g @aws/nx-plugin:ts#api --framework=smithy npx nx g @aws/nx-plugin:ts#api --framework=smithy bunx nx g @aws/nx-plugin:ts#api --framework=smithy- インストール Nx Console VSCode Plugin まだインストールしていない場合
- VSCodeでNxコンソールを開く
- クリック
Generate (UI)"Common Nx Commands"セクションで - 検索
@aws/nx-plugin - ts#api - 必須パラメータを入力
- framework: smithy
- クリック
Generate
コマンドを組み立てる10
必須
framework = smithy
name必須stringAPIの名前(必須)。クラス名とファイルパスの生成に使用されます。
frameworkenumデフォルト:trpc使用するAPIフレームワーク。
trpcsmithyintegrationPatternenumデフォルト:isolatedAPI用にAPI Gateway統合を生成する方法。isolated(デフォルト)またはsharedから選択します。
isolatedsharedauthenumデフォルト:iamAPIの認証に使用する方法。iam(デフォルト)、cognito、customから選択します。
iamcognitocustomdirectorystringデフォルト:packagesアプリケーションを保存するディレクトリ。
iacenumデフォルト:inherit優先するIaCプロバイダー。デフォルトでは初期選択から継承されます。
inheritcdkterraforminfraenumデフォルト:rest-lambdaこのAPIをデプロイするために使用するインフラストラクチャのタイプ。
rest-lambdanonenamespacestringframework = smithySmithy APIの名前空間(smithyフレームワークにのみ適用されます)。デフォルトはモノレポのスコープです
subDirectorystringプロジェクトが配置されるサブディレクトリ。デフォルトではプロジェクト名になります。
preferInstallDependenciesbooleanデフォルト:trueジェネレーター実行後に依存関係のインストールを優先するかどうか。複数のジェネレーターをバッチ処理する際にインストールを延期する場合は false に設定します(後続のジェネレーターが Nx プロジェクトグラフを計算できるよう、必要に応じてインストールは実行されます)。最後に一度だけインストールします。
ジェネレーターの出力
Section titled “ジェネレーターの出力”ジェネレーターは<directory>/<api-name>ディレクトリに2つの関連プロジェクトを作成します:
Directorymodel/ Smithyモデルプロジェクト
- project.json プロジェクト設定とビルドターゲット
- smithy-build.json Smithyビルド設定
- ssdk.rolldown.config.mjs 生成されたTypeScript Server SDKをバンドル
Directorysrc/
- main.smithy メインサービス定義
Directoryoperations/
- echo.smithy サンプルオペレーション定義
Directorybackend/ TypeScriptバックエンド実装
- package.json プロジェクトのパッケージ名と依存関係を定義するプロジェクトマニフェスト
- project.json プロジェクト設定とビルドターゲット
- rolldown.config.ts バンドル設定
- tsconfig.json TypeScript設定
- tsconfig.lib.json ライブラリソース用のTypeScript設定
- tsconfig.spec.json テスト用のTypeScript設定
- vitest.config.mts Vitest設定
Directorysrc/
- index.ts パッケージエントリーポイント
- handler.ts AWS Lambdaハンドラー
- local-server.ts ローカル開発サーバー
- service.ts サービス実装
- context.ts サービスコンテキスト定義
Directoryoperations/
- echo.ts サンプルオペレーション実装
Directorygenerated/ 生成されたTypeScript SDK(ビルド時に作成)
- …
インフラストラクチャ
Section titled “インフラストラクチャ”このジェネレーターは選択したiacに基づいてインフラストラクチャコードを作成するため、関連するCDKコンストラクトまたはTerraformモジュールを含むpackages/commonにプロジェクトを作成します。
共通のインフラストラクチャコードプロジェクトは次のように構成されています:
Directorypackages/common/constructs
Directorysrc
Directoryapp/ プロジェクト/ジェネレーター固有のインフラストラクチャ用のコンストラクト
Directoryapis/
- <project-name>.ts APIをデプロイするためのCDKコンストラクト
Directorycore/
appのコンストラクトで再利用される汎用コンストラクトDirectoryapi/
- rest-api.ts REST APIをデプロイするためのCDKコンストラクト
- utils.ts APIコンストラクト用のユーティリティ
- index.ts
appからコンストラクトをエクスポートするエントリーポイント
- project.json プロジェクトビルドターゲットと設定
Directorypackages/common/terraform
Directorysrc
Directoryapp/ プロジェクト/ジェネレーター固有のインフラストラクチャ用のTerraformモジュール
Directoryapis/
Directory<project-name>/
- <project-name>.tf APIをデプロイするためのモジュール
Directorycore/
appのモジュールで再利用される汎用モジュールDirectoryapi/
Directoryrest-api/
- rest-api.tf REST APIをデプロイするためのモジュール
- project.json プロジェクトビルドターゲットと設定
アーキテクチャ
Section titled “アーキテクチャ”デプロイされた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}シェイプライブラリの追加
Section titled “シェイプライブラリの追加”同じデータ型を共有する複数のSmithy APIがある場合、各モデルで重複させるのではなく、シェイプライブラリでそれらの型を一度定義できます。シェイプライブラリは、サービスを持たないSmithyプロジェクトで、再利用可能なシェイプのみを持ち、任意の数のSmithyプロジェクトが依存できます。
smithy#projectジェネレーターで生成します:
このジェネレーターを実行@aws/nx-plugin:smithy#project
pnpm nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes yarn nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes npx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes bunx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes- インストール Nx Console VSCode Plugin まだインストールしていない場合
- VSCodeでNxコンソールを開く
- クリック
Generate (UI)"Common Nx Commands"セクションで - 検索
@aws/nx-plugin - smithy#project - 必須パラメータを入力
- name: my-shapes
- type: shapes
- クリック
Generate
コマンドを組み立てる7
必須
APIのモデルはuseでそのシェイプを参照できます:
$version: "2.0"
namespace com.example.api
use com.example.shared#Customer
structure GetCustomerOutput { @required customer: Customer}シェイプライブラリを作成し、APIのモデルの依存関係として接続する方法については、Smithyプロジェクトガイドを参照してください。
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};サービスコンテキスト
Section titled “サービスコンテキスト”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による可観測性”ジェネレーターは、Middyミドルウェアによる自動コンテキストインジェクションを使用して、AWS Lambda Powertoolsを使用した構造化ロギングを設定します。
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'); // ...};トレーシング
Section titled “トレーシング”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); // ...};エラーハンドリング
Section titled “エラーハンドリング”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が認証によって保護されている場合、オペレーションは誰が呼び出しているかを知る必要があることがよくあります。推奨されるアプローチは、ハンドラーで呼び出し元のIDを一度解決し、特定のオペレーションで使用するためにサービスコンテキストを通じて渡すことです。
未承認のケースをSmithyエラーとしてモデル化し、適切な403レスポンスにシリアライズされるようにします。モデルに追加します(例:model/src/operations/errors.smithy)、そしてIDを必要とする任意のオペレーションで参照します:
$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のサービスコンテキストで解決されたIDを公開します。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.1146.0 --filter my-apiyarn workspace @my-scope/my-api add @aws-sdk/client-cognito-identity-provider@3.1146.0npm install --legacy-peer-deps @aws-sdk/client-cognito-identity-provider@3.1146.0 -w packages/my-api/backendbun add @aws-sdk/client-cognito-identity-provider@3.1146.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オーソライザーがないため、ローカル開発用のスタブIDを提供します:
const httpResponse = await serviceHandler.handle(httpRequest, { tracer, logger, metrics, getIdentity: async () => ({ sub: 'local', username: 'local' }),});これで、オペレーション内で解決されたIDを使用できます。例えば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プロジェクトは、miseを介してではなくsmithyを直接実行します。
または、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ターゲットのコマンドを編集することで、任意のプラットフォームでどちらかを選択できます。
miseの代わりにグローバルにインストールされたSmithy CLIを使用するには、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> --プレフィックスを復元します。
バンドルターゲット
Section titled “バンドルターゲット”ジェネレーターは、Rolldown を使用してデプロイメントパッケージを作成する bundle ターゲットを自動的に設定します:
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 は、定義されている場合、複数のバンドルを並列で作成することを管理します。
ローカル開発
Section titled “ローカル開発”ジェネレーターはホットリロード機能を持つローカル開発サーバーを設定します:
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モジュールを一度インスタンス化し、そのbucket_name出力をasset_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 endpoint (the custom domain URL, if one is configured)output "api_url" { value = module.my_api.api_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)を使用し、OWASP Top 10 を含む一般的な Web エクスプロイトに対する保護を提供します。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の場合、生成されたインフラストラクチャはデフォルトでアクセスログを有効にし、リクエストごとに1行の構造化JSONを専用のCloudWatch Logsグループに書き込みます。ログループは顧客管理のKMSキーで暗号化され、1年間保持されます。
API Gatewayは、アカウントレベルのCloudWatch Logsロールを使用してアクセスログを書き込みます。このロールはAWS::ApiGateway::Account設定で構成されており、これはリージョンごと、アカウントごとのシングルトンです。つまり、リージョン内のすべてのREST APIに対して1つのロールしか存在しません。独立してデプロイされる複数のスタック間でこれを安全に管理するため、生成されたインフラストラクチャは次のようになっています:
- 共有CloudWatch Logsロールを作成し、動作中のロールがまだ設定されていない場合にのみアカウントに構成するため、デプロイメントが他のスタックが所有するロールを上書きすることはありません。
- ティアダウン時にアカウント設定をそのままにするため、1つのスタックを破棄してもリージョン内の他の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ブロックを編集することで、アクセスログフォーマットをカスタマイズできます。
カスタムドメイン
Section titled “カスタムドメイン”デフォルトでは、APIはAPI Gatewayが生成するエンドポイント(https://<id>.execute-api.<region>.amazonaws.com/...)から提供されます。代わりに独自のドメインから提供するには、ドメイン名とそのACM証明書を指定します。カスタムドメインはデフォルトでリージョナルエンドポイントを使用するため、証明書はAPIと同じリージョンにある必要があります。デフォルトでは最小TLSバージョン1.2が使用されます。
カスタムドメインが設定されている場合、ランタイム設定のAPIのURLはカスタムドメインURLになるため、ウェブサイトや他のクライアントは生成されたエンドポイントではなく、あなたのドメインを呼び出します。
生成されたpackages/common/constructs/src/app/apis/my-api.tsで、ドメインをsuperに渡します:
import { Certificate } from 'aws-cdk-lib/aws-certificatemanager';
super(scope, id, { apiName: 'MyApi', // ... domainName: { domainName: 'api.example.com', certificate: Certificate.fromCertificateArn( scope, 'MyApiCertificate', 'arn:aws:acm:<region>:123456789012:certificate/...', ), }, ...props,});APIはステージパスプレフィックス(/prod/)なしで、ドメインのルートから提供されます。代わりにパスの下で提供するには、domainNameにbasePathを設定します。ランタイム設定のURLにはそれが含まれます。basePathはCDKでのみ利用可能です。Terraformモジュールは常にドメインのルートでAPIを提供します。
import { DomainName } from 'aws-cdk-lib/aws-apigatewayv2';import { Certificate } from 'aws-cdk-lib/aws-certificatemanager';
super(scope, id, { apiName: 'MyApi', // ... defaultDomainMapping: { domainName: new DomainName(scope, 'MyApiDomainName', { domainName: 'api.example.com', certificate: Certificate.fromCertificateArn( scope, 'MyApiCertificate', 'arn:aws:acm:<region>:123456789012:certificate/...', ), }), }, ...props,});カスタムドメインが設定されている場合、スタックはMyApiDomainNameAlias(DNSレコードを指すドメイン名)とMyApiDomainNameAliasHostedZoneIdを出力します。
APIモジュールにcustom_domain_nameとacm_certificate_arn変数を設定します:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api" # ...
custom_domain_name = "api.example.com" acm_certificate_arn = "arn:aws:acm:<region>:123456789012:certificate/..."}モジュールはcustom_domain_target_domain_name(DNSレコードを指すリージョナルドメイン名)とcustom_domain_hosted_zone_idを出力します。そのapi_url出力は、クライアントが呼び出すべきURLです:カスタムドメインが設定されている場合はカスタムドメインURL、それ以外の場合はステージ呼び出しURLです。
最後に、ドメインのDNSレコードを作成します。任意のDNSプロバイダーで、上記のドメイン名出力を指すCNAMEレコードを作成します。ドメインがRoute 53でホストされている場合は、代わりにホストゾーンID出力を使用してエイリアスレコードを作成できます。これはゾーンapex(例:example.com)でも機能します。
REST/HTTP API CDK コンストラクトは、各オペレーションのインテグレーションを定義するための型安全なインターフェースを提供するように構成されています。
デフォルトインテグレーション
Section titled “デフォルトインテグレーション”静的な 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 パターンでは、これによりオペレーションごとに 1 つの Lambda 関数が作成されます。
インテグレーションへのアクセス
Section titled “インテグレーションへのアクセス”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 パターンでは、モジュールの出力はオペレーション名でキー付けされたマップであるため、単一のオペレーションのリソースにアクセスできます。例えば、1 つのオペレーションの 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 パターンでは、関数が 1 つしかないため、代わりに同等の単数形の出力(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 メソッドを使用できます。例えば、1 つのオペレーションだけの 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 関数リソースは既にオペレーションごとになっているため、オプションはオペレーション名によって変更できます。例えば、1 つのオペレーションにより長いタイムアウトを与えるには、生成されたモジュール内の 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}インテグレーションのオーバーライド
Section titled “インテグレーションのオーバーライド”withOverrides メソッドを使用して、特定のオペレーションのインテグレーションをオーバーライドすることもできます。各オーバーライドは、HTTP または REST API の適切な CDK インテグレーションコンストラクトに型付けされた integration プロパティを指定する必要があります。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}"}オーソライザーのオーバーライド
Section titled “オーソライザーのオーバーライド”インテグレーションで 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 で 1 つのオペレーションを認証なしのままにするには:
resource "aws_apigatewayv2_route" "operation_routes" { for_each = local.operations
# ... rest of configuration
authorization_type = each.key == "getDocumentation" ? "NONE" : "AWS_IAM"}IAM 認証された REST API の場合は、そのオペレーションのパスへの認証なしアクセスを許可するリソースポリシーステートメントも追加してください。
明示的なインテグレーション
Section titled “明示的なインテグレーション”必要に応じて、デフォルトインテグレーションを使用せず、各オペレーションに直接インテグレーションを提供することもできます。これは、例えば各オペレーションが異なるタイプのインテグレーションを使用する必要がある場合や、新しいオペレーションを追加する際に型エラーを受け取りたい場合に便利です:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});isolated パターンで使用される for_each を、各オペレーションの Lambda 関数、インテグレーション、権限の明示的なインスタンス化に置き換えます。
インテグレーションパターン
Section titled “インテグレーションパターン”生成された API は 2 つのインテグレーションパターンをサポートしています:
isolatedは、オペレーションごとに 1 つの Lambda 関数を作成します。これは API のデフォルトで推奨されるオプションです。sharedは、単一のデフォルトルーター Lambda を作成し、特定のインテグレーションをオーバーライドしない限り、すべてのオペレーションでそれを再利用します。
isolated は、オペレーションごとにより細かい権限と設定を提供し、ログとトレースのより良い分離も提供します。shared は、使用頻度の低い API でコールドスタートに遭遇する可能性を減らします。
インテグレーションパターンは、API コンストラクトを更新することで CDK でいつでも変更できます。例えば、pattern を 'shared' に設定すると、オペレーションごとに 1 つではなく、単一の関数が作成されます:
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 Path Depth Limit
Section titled “Terraform REST API Path Depth Limit”オペレーションは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ジェネレーターを使用して、このプロジェクトをワークスペース内の他のプロジェクトと統合します。このプロジェクトに関連する次の接続があります: