tRPC
tRPCは、TypeScriptでエンドツーエンドの型安全性を備えたAPIを構築するためのフレームワークです。tRPCを使用すると、API操作の入力と出力の更新がクライアントコードに即座に反映され、プロジェクトを再ビルドする必要なくIDEで確認できます。
tRPC APIジェネレーターは、AWS CDKまたはTerraformインフラストラクチャのセットアップを含む新しいtRPC APIを作成します。生成されたバックエンドは、サーバーレスデプロイメントにAWS Lambdaを使用し、AWS API Gateway APIを介して公開され、Zodを使用したスキーマ検証が含まれます。また、ロギング、AWS X-Rayトレーシング、Cloudwatch Metricsを含む可観測性のためにAWS Lambda Powertoolsをセットアップします。
tRPC APIの生成
Section titled “tRPC APIの生成”新しいtRPC APIは2つの方法で生成できます:
このジェネレーターを実行@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
コマンドを組み立てる9
必須
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-lambdahttp-lambdanonesubDirectorystringプロジェクトが配置されるサブディレクトリ。デフォルトではプロジェクト名になります。
preferInstallDependenciesbooleanデフォルト:trueジェネレーター実行後に依存関係のインストールを優先するかどうか。複数のジェネレーターをバッチ処理する際にインストールを延期する場合は false に設定します(後続のジェネレーターが Nx プロジェクトグラフを計算できるよう、必要に応じてインストールは実行されます)。最後に一度だけインストールします。
ジェネレーター出力
Section titled “ジェネレーター出力”ジェネレーターは<directory>/<api-name>ディレクトリに以下のプロジェクト構造を作成します:
Directorysrc
- index.ts Package entrypoint re-exporting the router, context, client and schema
- init.ts Backend tRPC initialisation
- handler.ts Lambda handler entrypoint
- router.ts tRPC router definition
Directoryschema Schema definitions using Zod
- index.ts Barrel re-exporting every schema
- echo.ts Example definitions for the input and output of the “echo” procedure
- z-async-iterable.ts Zod helper for subscriptions (REST API only)
Directoryprocedures Procedures (or operations) exposed by your API
- echo.ts Example procedure
Directorymiddleware
- index.ts Barrel re-exporting the middleware, and the procedure context type
- error.ts Middleware for error handling
- logger.ts middleware for configuring AWS Powertools for Lambda logging
- tracer.ts middleware for configuring AWS Powertools for Lambda tracing
- metrics.ts middleware for configuring AWS Powertools for Lambda metrics
- local-server.ts tRPC standalone adapter entrypoint for local development server
Directoryclient
- index.ts Type-safe client for machine-to-machine API calls
- rolldown.config.ts Bundle configuration for the Lambda deployment package
- tsconfig.json TypeScript configuration
- tsconfig.lib.json TypeScript configuration for the library sources
- tsconfig.spec.json TypeScript configuration for the tests
- vitest.config.mts Vitest configuration
- package.json Project manifest defining the project’s package name and dependencies
- project.json Project configuration and build targets
- README.md Project readme
- .gitignore Ignores the project’s build output
インフラストラクチャ
Section titled “インフラストラクチャ”このジェネレーターは、選択した iac に基づいてインフラストラクチャをコードとして提供するため、関連する CDK コンストラクトまたは Terraform モジュールを含む packages/common にプロジェクトを作成します。
共通のインフラストラクチャコードプロジェクトは、次のように構成されています:
Directorypackages/common/constructs
Directorysrc
Directoryapp/ プロジェクト/ジェネレーター固有のインフラストラクチャ用のコンストラクト
- …
Directorycore/
app内のコンストラクトによって再利用される汎用コンストラクト- …
- index.ts
appからコンストラクトをエクスポートするエントリーポイント
- project.json プロジェクトのビルドターゲットと設定
Directorypackages/common/terraform
Directorysrc
Directoryapp/ プロジェクト/ジェネレーター固有のインフラストラクチャ用の Terraform モジュール
- …
Directorycore/
app内のモジュールによって再利用される汎用モジュール- …
- project.json プロジェクトのビルドターゲットと設定
APIをデプロイするために、以下のファイルが生成されます:
Directorypackages/common/constructs/src
Directoryapp
Directoryapis
- <project-name>.ts CDK construct for deploying your API
Directorycore
Directoryapi
- http-api.ts CDK construct for deploying an HTTP API (if you selected to deploy an HTTP API)
- rest-api.ts CDK construct for deploying a REST API (if you selected to deploy a REST API)
- utils.ts Utilities for the API constructs
Directorypackages/common/terraform/src
Directoryapp
Directoryapis
Directory<project-name>
- <project-name>.tf Module for deploying your API
Directorycore
Directoryapi
Directoryhttp-api
- http-api.tf Module for deploying an HTTP API (if you selected to deploy an HTTP API)
Directoryrest-api
- rest-api.tf Module for deploying a REST API (if you selected to deploy a REST API)
アーキテクチャ
Section titled “アーキテクチャ”デプロイされたアプリケーションは以下のアーキテクチャを持ちます:ハンドラーを実行するLambda関数の前にAPI Gateway APIが配置されます。
REST APIには、API Gatewayステージの前にAWS WAFv2 Web ACLが含まれており、AWSマネージドのデフォルトルールセットが有効になっています。
HTTP APIは直接WAFをサポートしていません。WAF保護が必要な場合は、代わりにREST APIを選択するか、HTTP APIの前にCloudFrontディストリビューションを配置してください。
tRPC APIの実装
Section titled “tRPC APIの実装”高レベルでは、tRPC APIはリクエストを特定のプロシージャに委譲するルーターで構成されます。各プロシージャには、Zodスキーマとして定義された入力と出力があります。
src/schemaディレクトリには、クライアントとサーバーコード間で共有される型が含まれています。このパッケージでは、これらの型はTypeScriptファーストのスキーマ宣言および検証ライブラリであるZodを使用して定義されています。
スキーマの例は次のようになります:
import { z } from 'zod';
// Schema definitionexport const UserSchema = z.object({ name: z.string(), height: z.number(), dateOfBirth: z.string().datetime(),});
// Corresponding TypeScript typeexport type User = z.TypeOf<typeof UserSchema>;上記のスキーマを考えると、User型は次のTypeScriptと同等です:
interface User { name: string; height: number; dateOfBirth: string;}スキーマはサーバーとクライアントの両方のコードで共有されるため、APIで使用される構造を変更する際の単一の場所を提供します。
スキーマはtRPC APIによって実行時に自動的に検証されるため、バックエンドでカスタム検証ロジックを手作業で作成する必要がありません。
Zodは、.merge、.pick、.omitなどのスキーマを組み合わせたり派生させたりするための強力なユーティリティを提供します。詳細については、Zodドキュメントウェブサイトを参照してください。
ルーターとプロシージャ
Section titled “ルーターとプロシージャ”tRPCルーターはsrc/router.tsで定義され、すべてのプロシージャを登録します。各プロシージャは、期待される入力、出力、および実装を定義します。Lambdaハンドラーのエントリーポイントはsrc/handler.tsにあり、リクエストをルーターに転送します。
生成されたサンプルルーターには、echoという単一の操作があります:
import { echo } from './procedures/echo.js';
export const appRouter = router({ echo,});例のechoプロシージャはsrc/procedures/echo.tsに生成されます:
export const echo = publicProcedure .input(EchoInputSchema) .output(EchoOutputSchema) .query((opts) => ({ message: opts.input.message }));上記を分解すると:
publicProcedureは、src/middlewareでセットアップされたミドルウェアを含む、API上のパブリックメソッドを定義します。このミドルウェアには、ロギング、トレーシング、メトリクスのためのAWS Lambda Powertools統合が含まれます。inputは、操作の期待される入力を定義するZodスキーマを受け入れます。この操作に送信されたリクエストは、このスキーマに対して自動的に検証されます。outputは、操作の期待される出力を定義するZodスキーマを受け入れます。スキーマに準拠しない出力を返すと、実装で型エラーが表示されます。queryは、APIの実装を定義する関数を受け入れます。この実装は、操作に渡されたinputを含むoptsと、ミドルウェアによってセットアップされた他のコンテキストをopts.ctxで受け取ります。queryに渡される関数は、outputスキーマに準拠する出力を返す必要があります。
実装を定義するためのqueryの使用は、操作が変更を伴わないことを示します。これを使用してデータを取得するメソッドを定義します。変更を伴う操作を実装するには、代わりにmutationメソッドを使用します。
新しいプロシージャを追加する場合は、src/router.tsのルーターに追加して登録してください。
サブスクリプション(ストリーミング)
Section titled “サブスクリプション(ストリーミング)”tRPCサブスクリプションを使用すると、Server-Sent Events (SSE)を使用してサーバーからクライアントにデータをストリーミングできます。コンピュートタイプとしてrest-lambdaを選択すると、ジェネレーターはストリーミングに必要なインフラストラクチャ、ストリーミングLambdaハンドラー、およびZodAsyncIterableスキーマヘルパーを自動的に構成します。
サブスクリプションプロシージャを定義するには、非同期ジェネレーター関数で.subscriptionメソッドを使用します。src/schema/z-async-iterable.tsのZodAsyncIterableヘルパーを使用して出力スキーマを定義します:
import { publicProcedure } from '../init.js';import { z } from 'zod';import { ZodAsyncIterable } from '../schema/z-async-iterable.js';
const InputSchema = z.object({ query: z.string() });const ChunkSchema = z.object({ text: z.string() });
export const myStream = publicProcedure .input(InputSchema) .output( ZodAsyncIterable({ yield: ChunkSchema, }), ) .subscription(async function* (opts) { // Yield data to the client as it becomes available for (const chunk of await getResults(opts.input.query)) { yield { text: chunk }; } });他のプロシージャと同様に、ルーターにサブスクリプションを登録します:
export const appRouter = router({ echo, myStream,});生成されたインフラストラクチャは、すべてのREST API操作に対してAPI GatewayでResponseTransferMode.STREAMを使用したストリーミングLambdaハンドラーを使用し、サブスクリプションが通常のクエリとミューテーションと並行して動作できるようにします。
tRPC APIのカスタマイズ
Section titled “tRPC APIのカスタマイズ”実装では、TRPCErrorをスローすることで、クライアントにエラーレスポンスを返すことができます。これらは、エラーのタイプを示すcodeを受け入れます。例えば:
throw new TRPCError({ code: 'NOT_FOUND', message: 'The requested resource could not be found',});APIが成長するにつれて、関連する操作をグループ化したい場合があります。
ネストされたルーターを使用して操作をグループ化できます。例えば:
import { getUser } from './procedures/users/get.js';import { listUsers } from './procedures/users/list.js';
const appRouter = router({ users: router({ get: getUser, list: listUsers, }), ...})クライアントはこの操作のグループ化を受け取ります。例えば、この場合のlistUsers操作の呼び出しは次のようになります:
client.users.list.query();AWS Lambda Powertoolsロガーはsrc/middleware/logger.tsで構成されており、API実装ではopts.ctx.loggerを介してアクセスできます。これを使用してCloudWatch Logsにログを記録したり、すべての構造化ログメッセージに含める追加の値を制御したりできます。例えば:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.logger.info('Operation called with input', opts.input);
return ...; });ロガーの詳細については、AWS Lambda Powertoolsロガードキュメントを参照してください。
メトリクスの記録
Section titled “メトリクスの記録”AWS Lambda Powertoolsメトリクスはsrc/middleware/metrics.tsで構成されており、API実装ではopts.ctx.metricsを介してアクセスできます。これを使用して、AWS SDKをインポートして使用する必要なく、CloudWatchにメトリクスを記録できます。例えば:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.metrics.addMetric('Invocations', 'Count', 1);
return ...; });詳細については、AWS Lambda Powertoolsメトリクスドキュメントを参照してください。
X-Rayトレーシングの微調整
Section titled “X-Rayトレーシングの微調整”AWS Lambda Powertoolsトレーサーはsrc/middleware/tracer.tsで構成されており、API実装ではopts.ctx.tracerを介してアクセスできます。これを使用して、AWS X-Rayでトレースを追加し、APIリクエストのパフォーマンスとフローに関する詳細な洞察を提供できます。例えば:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { const subSegment = opts.ctx.tracer.getSegment()!.addNewSubsegment('MyAlgorithm'); // ... my algorithm logic to capture subSegment.close();
return ...; });詳細については、AWS Lambda Powertoolsトレーサードキュメントを参照してください。
カスタムミドルウェアの実装
Section titled “カスタムミドルウェアの実装”ミドルウェアを実装することで、プロシージャに提供されるコンテキストに追加の値を追加できます。
例として、src/middleware/identity.tsでAPIの呼び出し元ユーザーに関する詳細を抽出するミドルウェアを実装しましょう。
この例では、IAM認証のIDミドルウェアについて説明します。API Gatewayイベントから抽出されたsubを使用して、Cognitoで呼び出し元を検索します。
検索にはCognito Identity Providerクライアントを使用しますが、これは生成されたtRPC APIの依存関係ではありません。まず、APIプロジェクトにインストールしてください:
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-apibun add @aws-sdk/client-cognito-identity-provider@3.1126.0 --cwd packages/my-apiまず、コンテキストに追加する内容を定義します:
export interface IIdentityContext { identity?: { sub: string; username: string; };}コンテキストに追加の_オプション_プロパティを定義することに注意してください。tRPCは、このミドルウェアを正しく構成したプロシージャでこれが定義されていることを保証します。
次に、ミドルウェア自体を実装します。これには次の構造があります:
export const createIdentityPlugin = () => { const t = initTRPC.context<...>().create(); return t.procedure.use(async (opts) => { // Add logic here to run before the procedure
const response = await opts.next(...);
// Add logic here to run after the procedure
return response; });};この場合、呼び出し元のCognitoユーザーに関する詳細を抽出したいと思います。API GatewayイベントからユーザーのサブジェクトID(または「sub」)を抽出し、Cognitoからユーザーの詳細を取得します。実装は、イベントがREST APIまたはHTTP APIのどちらから関数に提供されたかによって異なります:
import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';import { initTRPC, TRPCError } from '@trpc/server';import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import type { APIGatewayProxyEvent } from 'aws-lambda';
export interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC .context< IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEvent> >() .create();
const cognito = new CognitoIdentityProvider();
return t.procedure.use(async (opts) => { const cognitoAuthenticationProvider = opts.ctx.event.requestContext?.identity?.cognitoAuthenticationProvider;
let sub: string | undefined = undefined; if (cognitoAuthenticationProvider) { const providerParts = cognitoAuthenticationProvider.split(':'); sub = providerParts[providerParts.length - 1]; }
if (!sub) { throw new TRPCError({ code: 'FORBIDDEN', 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 TRPCError({ code: 'FORBIDDEN', message: `No user found with subjectId ${sub}`, }); }
// Provide the identity to other procedures in the context return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username: Users[0].Username!, }, }, }); });};import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';import { initTRPC, TRPCError } from '@trpc/server';import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import type { APIGatewayProxyEventV2WithIAMAuthorizer } from 'aws-lambda';
export interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC .context< IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEventV2WithIAMAuthorizer> >() .create();
const cognito = new CognitoIdentityProvider();
return t.procedure.use(async (opts) => { const cognitoIdentity = opts.ctx.event.requestContext?.authorizer?.iam ?.cognitoIdentity as unknown as | { amr: string[]; } | undefined;
const sub = (cognitoIdentity?.amr ?? []) .flatMap((s) => (s.includes(':CognitoSignIn:') ? [s] : [])) .map((s) => { const parts = s.split(':'); return parts[parts.length - 1]; })?.[0];
if (!sub) { throw new TRPCError({ code: 'FORBIDDEN', 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 TRPCError({ code: 'FORBIDDEN', message: `No user found with subjectId ${sub}`, }); }
// Provide the identity to other procedures in the context return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username: Users[0].Username!, }, }, }); });};auth: 'cognito'でデプロイすると、API Gateway Cognitoオーソライザーは、呼び出し元がAuthorizationヘッダーで提供するJWTを検証し、検証されたクレームをLambdaイベントに配置します。ミドルウェアはこれらのクレームを読み取るだけで、追加のAWS SDK呼び出しや手動のJWT検証は必要ありません。
まず、コンテキストに追加する内容を定義します:
export interface IIdentityContext { identity?: { sub: string; username: string; };}コンテキストに追加の_オプション_プロパティを定義することに注意してください。tRPCは、このミドルウェアを正しく構成したプロシージャでこれが定義されていることを保証します。
次に、ミドルウェア自体です。イベントタイプとクレームの場所はREST APIとHTTP APIで異なるため、実装は選択したinfraによって異なります:
REST APIのCognito User Poolsオーソライザーは、クレームをevent.requestContext.authorizer.claimsに配置します:
import { initTRPC, TRPCError } from '@trpc/server';import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import type { APIGatewayProxyEvent } from 'aws-lambda';
export interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC .context< IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEvent> >() .create();
return t.procedure.use(async (opts) => { const claims = opts.ctx.event.requestContext?.authorizer?.claims as | Record<string, string> | undefined;
const sub = claims?.sub; const username = claims?.username ?? claims?.['cognito:username'];
if (!sub || !username) { throw new TRPCError({ code: 'FORBIDDEN', message: 'Unable to determine calling user', }); }
return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username, }, }, }); });};HTTP APIのJWTオーソライザーは、クレームが1レベル深いevent.requestContext.authorizer.jwt.claimsにあるpayload-v2イベントを配信します。コンテキストは、生成されたpublicProcedureが使用するものと一致するようにAPIGatewayProxyEventV2WithJWTAuthorizerで型付けする必要があります。そうしないと、.concat()がtRPCのContext mismatchエラーで失敗します:
import { initTRPC, TRPCError } from '@trpc/server';import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import type { APIGatewayProxyEventV2WithJWTAuthorizer } from 'aws-lambda';
export interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC .context< IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEventV2WithJWTAuthorizer> >() .create();
return t.procedure.use(async (opts) => { const claims = opts.ctx.event.requestContext?.authorizer?.jwt?.claims as | Record<string, string> | undefined;
const sub = claims?.sub; const username = claims?.username ?? claims?.['cognito:username'];
if (!sub || !username) { throw new TRPCError({ code: 'FORBIDDEN', message: 'Unable to determine calling user', }); }
return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username, }, }, }); });};その後、呼び出し元のIDが必要なプロシージャにプラグインをミックスできます:
import { publicProcedure } from '../init.js';import { createIdentityPlugin } from '../middleware/identity.js';import { z } from 'zod';
export const me = publicProcedure .concat(createIdentityPlugin()) .output(z.object({ sub: z.string(), username: z.string() })) .query(({ ctx }) => ({ sub: ctx.identity!.sub, username: ctx.identity!.username, }));tRPC APIのデプロイ
Section titled “tRPC APIのデプロイ”tRPC APIジェネレーターは、選択したiacに基づいてCDKまたはTerraformのインフラストラクチャコードを作成します。これを使用してtRPC APIをデプロイできます。
APIをデプロイするためのCDKコンストラクトはcommon/constructsフォルダにあります。これをCDKアプリケーションで使用できます。例えば:
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(), }); }}import { MyApi, UserIdentity } from '@my-scope/common-constructs';
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { // Add the api to your stack const identity = new UserIdentity(this, 'Identity');
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), identity, }); }}UserIdentityコンストラクトは、ts#website#authジェネレーターを使用して生成できます。
これにより、AWS API Gateway RESTまたはHTTP API、ビジネスロジック用のAWS Lambda関数、および選択したauthメソッドに基づく認証を含むAPIインフラストラクチャがセットアップされます。
APIをデプロイするためのTerraformモジュールはcommon/terraformフォルダにあります。これをTerraform構成で使用できます。
APIモジュールは、共有S3アセットバケットにLambdaデプロイメントzipをステージングします。詳細については、Terraformインフラストラクチャガイドを参照してください。デプロイメントごとにcore/asset-bucketモジュールを1回インスタンス化し、その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}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
user_pool_id = local.user_pool_id user_pool_client_ids = [local.client_id]
# 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}適切なTerraformリソースまたはモジュールを使用して、Cognito User PoolとClientをセットアップできます。
これにより、以下がセットアップされます:
- すべてのtRPCプロシージャを提供するAWS Lambda関数
- 関数トリガーとしてのAPI Gateway HTTP/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}
# Access IAM role for granting additional permissionsoutput "lambda_execution_role_arn" { value = module.my_api.lambda_execution_role_arn}モジュールに変数を渡すことで、CORS設定をカスタマイズできます:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Custom CORS configuration cors_allow_origins = ["https://myapp.com", "https://staging.myapp.com"] cors_allow_methods = ["GET", "POST", "PUT", "DELETE"] cors_allow_headers = [ "authorization", "content-type", "x-custom-header" ]
tags = local.common_tags}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ブロックを編集することで、アクセスログフォーマットをカスタマイズできます。
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”アクセスの付与(IAMのみ)
Section titled “アクセスの付与(IAMのみ)”次のように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 tRPC 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 role (e.g., for authenticated users)resource "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}
# Or attach to an existing role by nameresource "aws_iam_role_policy_attachment" "api_invoke_access_existing" { role = "MyExistingRole" policy_arn = aws_iam_policy.api_invoke_policy.arn}IAMポリシーに使用できるAPIモジュールの主要な出力は次のとおりです:
module.my_api.api_execution_arn- execute-api:Invoke権限を付与するためmodule.my_api.api_arn- API Gateway ARNmodule.my_api.lambda_function_arn- Lambda関数ARN
バンドルターゲット
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 は、定義されている場合、複数のバンドルを並列で作成することを管理します。
ローカルtRPCサーバー
Section titled “ローカルtRPCサーバー”serveターゲットを使用して、APIのローカルサーバーを実行できます。例えば:
pnpm nx serve my-apiyarn nx serve my-apinpx nx serve my-apibunx nx serve my-apiローカルサーバーのエントリーポイントはsrc/local-server.tsです。
これにより、APIに変更を加えると自動的にリロードされます。
tRPC APIの呼び出し
Section titled “tRPC APIの呼び出し”tRPCクライアントを作成して、型安全な方法でAPIを呼び出すことができます。別のバックエンドからtRPC APIを呼び出す場合は、src/client/index.tsのクライアントを使用できます。例えば:
import { createMyApiClient } from '@my-scope/my-api';
const client = createMyApiClient({ url: 'https://my-api-url.example.com/' });
await client.echo.query({ message: 'Hello world!' });ReactウェブサイトからAPIを呼び出す場合は、Connectionジェネレーターを使用してクライアントを構成することを検討してください。
tRPCの詳細については、tRPCドキュメントを参照してください。
connectionジェネレーターを使用して、このプロジェクトをワークスペース内の他のプロジェクトと統合します。このプロジェクトに関連する接続は次のとおりです: