AWSのPDKからの移行
このガイドでは、AWS PDKプロジェクトをNx Plugin for AWSに移行する具体例と、このトピックに関する一般的なガイダンスを提供します。
PDKからNx Plugin for AWSへの移行により、以下のメリットが得られます:
- ビルドの高速化
- 使いやすさの向上(UIとCLI)
- バイブコーディング対応(MCPサーバーを試してみよう!)
- モダンな技術スタック
- ローカルでのAPI/ウェブサイト開発
- 制御性の向上(ベンダードファイルのカスタマイズ可能)
- その他多数
移行例: ショッピングリストアプリケーション
Section titled “移行例: ショッピングリストアプリケーション”このガイドではPDKチュートリアルのショッピングリストアプリケーションを移行対象として使用します。実際に手順を追いたい場合は、まず上記チュートリアルで対象プロジェクトを作成してください。
ショッピングリストアプリケーションは以下のPDKプロジェクトタイプで構成されています:
MonorepoTsProject
TypeSafeApiProject
CloudscapeReactTsWebsiteProject
InfrastructureTsProject
ワークスペースの作成
Section titled “ワークスペースの作成”最初に、新しいプロジェクト用のワークスペースを作成します。既存環境での移行よりも極端なアプローチですが、最もクリーンな結果を得られます。Nxワークスペースの作成は、PDKのMonorepoTsProject
に相当します:
npx create-nx-workspace@21.4.1 shopping-list --pm=pnpm --preset=@aws/nx-plugin@0.50.0 --iacProvider=CDK --ci=skip
npx create-nx-workspace@21.4.1 shopping-list --pm=yarn --preset=@aws/nx-plugin@0.50.0 --iacProvider=CDK --ci=skip
npx create-nx-workspace@21.4.1 shopping-list --pm=npm --preset=@aws/nx-plugin@0.50.0 --iacProvider=CDK --ci=skip
npx create-nx-workspace@21.4.1 shopping-list --pm=bun --preset=@aws/nx-plugin@0.50.0 --iacProvider=CDK --ci=skip
このコマンドで作成されたshopping-list
ディレクトリを、お気に入りのIDEで開きます。
APIの移行
Section titled “APIの移行”ショッピングリストアプリケーションで使用されたTypeSafeApiProject
は以下を利用していました:
- モデリング言語としてSmithy
- オペレーション実装用のTypeScript
- Reactウェブサイト連携用のTypeScriptフック生成
したがって、同等の機能を提供するためにts#smithy-api
ジェネレーターを使用できます。
TypeScript Smithy APIの生成
Section titled “TypeScript Smithy APIの生成”packages/api
にAPIプロジェクトをセットアップするため、ts#smithy-api
ジェネレーターを実行します:
- インストール Nx Console VSCode Plugin まだインストールしていない場合
- VSCodeでNxコンソールを開く
- クリック
Generate (UI)
"Common Nx Commands"セクションで - 検索
@aws/nx-plugin - ts#smithy-api
- 必須パラメータを入力
- name: api
- namespace: com.aws
- auth: IAM
- クリック
Generate
pnpm nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive
yarn nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive
npx nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive
bunx nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive
変更されるファイルを確認するためにドライランを実行することもできます
pnpm nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive --dry-run
これによりmodel
プロジェクトとbackend
プロジェクトが生成されます。model
プロジェクトにはSmithyモデルが含まれ、backend
にはサーバー実装が含まれます。
バックエンドはSmithy Server Generator for TypeScriptを使用しています。これについては後ほど詳しく説明します。
Smithyモデルの移行
Section titled “Smithyモデルの移行”Smithy APIプロジェクトの基本構造ができたので、モデルを移行します:
-
packages/api/model/src
内の生成されたサンプルSmithyファイルを削除 -
PDKプロジェクトの
packages/api/model/src/main/smithy
ディレクトリから新しいプロジェクトのpackages/api/model/src
ディレクトリへモデルをコピー -
smithy-build.json
のサービス名と名前空間をPDKアプリケーションに合わせて更新:smithy-build.json "plugins": {"openapi": {"service": "com.aws#MyApi",... -
main.smithy
のサービスにValidationException
エラーを追加(Smithy TypeScript Server SDK使用時に必要):main.smithy use smithy.framework#ValidationException/// My Shopping List API@restJson1service MyApi {version: "1.0"operations: [GetShoppingListsPutShoppingListDeleteShoppingList]errors: [BadRequestErrorNotAuthorizedErrorInternalFailureErrorValidationException]} -
packages/api/model/src
にextensions.smithy
ファイルを追加(生成クライアントにページネーション情報を提供するトレイト定義):extensions.smithy $version: "2"namespace com.awsuse smithy.openapi#specificationExtension@trait@specificationExtension(as: "x-cursor")structure cursor {inputToken: Stringenabled: Boolean} -
get-shopping-lists.smithy
のGetShoppingLists
オペレーションに新しい@cursor
トレイトを追加:operations/get-shopping-lists.smithy @readonly@http(method: "GET", uri: "/shopping-list")@paginated(inputToken: "nextToken", outputToken: "nextToken", pageSize: "pageSize", items: "shoppingLists")@cursor(inputToken: "nextToken")@handler(language: "typescript")operation GetShoppingLists {input := with [PaginatedInputMixin] {@httpQuery("shoppingListId")shoppingListId: ShoppingListId}Nx Plugin for AWS(
api-connection
ジェネレーター経由)のクライアントジェネレーターを使用する場合、@paginated
オペレーションには@cursor
も使用する必要があります。 -
最後に、すべてのオペレーションから
@handler
トレイトを削除(Nx Plugin for AWSではサポートされていないため)。ts#smithy-api
を使用する場合、このトレイトが生成するLambda関数CDKコンストラクトとバンドリングターゲットは不要です(すべてのLambda関数に単一バンドルを使用)。
ここでビルドを実行し、モデル変更を確認するとともに生成されたサーバーコードを確認できます。バックエンドプロジェクト(@shopping-list/api
)でいくつかのエラーが発生しますが、次に対処します。
pnpm nx run-many --target build
yarn nx run-many --target build
npx nx run-many --target build
bunx nx run-many --target build
Lambdaハンドラーの移行
Section titled “Lambdaハンドラーの移行”api/backend
プロジェクトは、Type Safe APIのapi/handlers/typescript
プロジェクトとある程度同等とみなせます。
Type Safe APIとts#smithy-api
ジェネレーターの主な違いの1つは、ハンドラーがSmithy Server Generator for TypeScriptを使用して実装される点です(Type Safe API独自の生成されたハンドラーラッパーではなく)。
ショッピングリストアプリケーションのLambdaハンドラーは@aws-sdk/client-dynamodb
パッケージに依存しているため、まずこれをインストールします:
pnpm add -w @aws-sdk/client-dynamodb
yarn add @aws-sdk/client-dynamodb
npm install --legacy-peer-deps @aws-sdk/client-dynamodb
bun install @aws-sdk/client-dynamodb
次に、PDKプロジェクトのhandlers/src/dynamo-client.ts
ファイルをbackend/src/operations
にコピーし、ハンドラーで利用可能にします。
ハンドラーの移行には以下の一般的な手順に従います:
-
PDKプロジェクトの
packages/api/handlers/typescript/src
ディレクトリから新しいプロジェクトのpackages/api/backend/src/operations
ディレクトリへハンドラーをコピー -
my-api-typescript-runtime
インポートを削除し、代わりに生成されたTypeScript Server SDKからオペレーションタイプとServiceContext
をインポート:import {deleteShoppingListHandler,DeleteShoppingListChainedHandlerFunction,INTERCEPTORS,Response,LoggingInterceptor,} from 'myapi-typescript-runtime';import { DeleteShoppingList as DeleteShoppingListOperation } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js'; -
ハンドラーラッパーのエクスポートを削除:
export const handler = deleteShoppingListHandler(...INTERCEPTORS,deleteShoppingList,); -
オペレーションハンドラーのシグネチャをSSDK用に更新:
export const deleteShoppingList: DeleteShoppingListChainedHandlerFunction = async (request) => {export const DeleteShoppingList: DeleteShoppingListOperation<ServiceContext> = async (input, ctx) => { -
LoggingInterceptor
の使用をctx.logger
に置換(メトリクスとトレーシングインターセプターも同様):LoggingInterceptor.getLogger(request).info('...');ctx.logger.info('...'); -
入力パラメーターへの参照を更新。SSDKはSmithyモデルに完全に一致するタイプを提供するため、入力参照を適宜更新:
const shoppingListId = request.input.requestParameters.shoppingListId;const shoppingListId = input.shoppingListId; -
Response
の使用を削除。SSDKではプレーンオブジェクトを返します:return Response.success({ shoppingListId });return { shoppingListId };Response
のスローや返信も行わず、代わりにSSDKが生成したエラーをスロー:throw Response.badRequest({ message: 'oh no' });return Response.badRequest({ message: 'oh no' });import { BadRequestError } from '../generated/ssdk/index.js';throw new BadRequestError({ message: 'oh no' }); -
相対インポートに
.js
拡張子を追加してESM構文を使用するようインポートを更新 -
service.ts
にオペレーションを追加:service.ts import { ServiceContext } from './context.js';import { MyApiService } from './generated/ssdk/index.js';import { DeleteShoppingList } from './operations/delete-shopping-list.js';import { GetShoppingLists } from './operations/get-shopping-lists.js';import { PutShoppingList } from './operations/put-shopping-list.js';// オペレーションをサービスに登録export const Service: MyApiService<ServiceContext> = {PutShoppingList,GetShoppingLists,DeleteShoppingList,};
ショッピングリストハンドラー移行
Delete Shopping List
import { DeleteItemCommand } from '@aws-sdk/client-dynamodb';import { deleteShoppingListHandler, DeleteShoppingListChainedHandlerFunction, INTERCEPTORS, Response, LoggingInterceptor,} from 'myapi-typescript-runtime';import { ddbClient } from './dynamo-client';
/** * Type-safe handler for the DeleteShoppingList operation */export const deleteShoppingList: DeleteShoppingListChainedHandlerFunction = async (request) => { LoggingInterceptor.getLogger(request).info( 'Start DeleteShoppingList Operation', );
const shoppingListId = request.input.requestParameters.shoppingListId; await ddbClient.send( new DeleteItemCommand({ TableName: 'shopping_list', Key: { shoppingListId: { S: shoppingListId, }, }, }), );
return Response.success({ shoppingListId, });};
/** * Entry point for the AWS Lambda handler for the DeleteShoppingList operation. * The deleteShoppingListHandler method wraps the type-safe handler and manages marshalling inputs and outputs */export const handler = deleteShoppingListHandler( ...INTERCEPTORS, deleteShoppingList,);
import { DeleteItemCommand } from '@aws-sdk/client-dynamodb';import { ddbClient } from './dynamo-client.js';import { DeleteShoppingList as DeleteShoppingListOperation } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js';
/** * Type-safe handler for the DeleteShoppingList operation */export const DeleteShoppingList: DeleteShoppingListOperation<ServiceContext> = async (input, ctx) => { ctx.logger.info( 'Start DeleteShoppingList Operation', );
const shoppingListId = input.shoppingListId; await ddbClient.send( new DeleteItemCommand({ TableName: 'shopping_list', Key: { shoppingListId: { S: shoppingListId!, }, }, }), );
return { shoppingListId, };};
Get Shopping Lists
import { DynamoDBClient, QueryCommand, QueryCommandInput, ScanCommand, ScanCommandInput } from '@aws-sdk/client-dynamodb';import { getShoppingListsHandler, GetShoppingListsChainedHandlerFunction, INTERCEPTORS, Response, LoggingInterceptor, ShoppingList,} from 'myapi-typescript-runtime';import { ddbClient } from './dynamo-client';
/** * Type-safe handler for the GetShoppingLists operation */export const getShoppingLists: GetShoppingListsChainedHandlerFunction = async (request) => { LoggingInterceptor.getLogger(request).info('Start GetShoppingLists Operation');
const nextToken = request.input.requestParameters.nextToken; const pageSize = request.input.requestParameters.pageSize; const shoppingListId = request.input.requestParameters.shoppingListId; const commandInput: ScanCommandInput | QueryCommandInput = { TableName: 'shopping_list', ConsistentRead: true, Limit: pageSize, ExclusiveStartKey: nextToken ? fromToken(nextToken) : undefined, ...(shoppingListId ? { KeyConditionExpression: 'shoppingListId = :shoppingListId', ExpressionAttributeValues: { ':shoppingListId': { S: request.input.requestParameters.shoppingListId!, }, }, } : {}), }; const response = await ddbClient.send(shoppingListId ? new QueryCommand(commandInput) : new ScanCommand(commandInput));
return Response.success({ shoppingLists: (response.Items || []) .map<ShoppingList>(item => ({ shoppingListId: item.shoppingListId.S!, name: item.name.S!, shoppingItems: JSON.parse(item.shoppingItems.S || '[]'), })), nextToken: response.LastEvaluatedKey ? toToken(response.LastEvaluatedKey) : undefined, });};
/** * Decode a stringified token * @param token a token passed to the paginated request */const fromToken = <T>(token?: string): T | undefined => token ? (JSON.parse(Buffer.from(decodeURIComponent(token), 'base64').toString()) as T) : undefined;
/** * Encode pagination details into an opaque stringified token * @param paginationToken pagination token details */const toToken = <T>(paginationToken?: T): string | undefined => paginationToken ? encodeURIComponent(Buffer.from(JSON.stringify(paginationToken)).toString('base64')) : undefined;
/** * Entry point for the AWS Lambda handler for the GetShoppingLists operation. * The getShoppingListsHandler method wraps the type-safe handler and manages marshalling inputs and outputs */export const handler = getShoppingListsHandler(...INTERCEPTORS, getShoppingLists);
import { QueryCommand, QueryCommandInput, ScanCommand, ScanCommandInput } from '@aws-sdk/client-dynamodb';import { ddbClient } from './dynamo-client.js';import { GetShoppingLists as GetShoppingListsOperation, ShoppingList } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js';
/** * Type-safe handler for the GetShoppingLists operation */export const GetShoppingLists: GetShoppingListsOperation<ServiceContext> = async (input, ctx) => { ctx.logger.info('Start GetShoppingLists Operation');
const nextToken = input.nextToken; const pageSize = input.pageSize; const shoppingListId = input.shoppingListId; const commandInput: ScanCommandInput | QueryCommandInput = { TableName: 'shopping_list', ConsistentRead: true, Limit: pageSize, ExclusiveStartKey: nextToken ? fromToken(nextToken) : undefined, ...(shoppingListId ? { KeyConditionExpression: 'shoppingListId = :shoppingListId', ExpressionAttributeValues: { ':shoppingListId': { S: input.shoppingListId!, }, }, } : {}), }; const response = await ddbClient.send(shoppingListId ? new QueryCommand(commandInput) : new ScanCommand(commandInput));
return { shoppingLists: (response.Items || []) .map<ShoppingList>(item => ({ shoppingListId: item.shoppingListId.S!, name: item.name.S!, shoppingItems: JSON.parse(item.shoppingItems.S || '[]'), })), nextToken: response.LastEvaluatedKey ? toToken(response.LastEvaluatedKey) : undefined, };};
/** * Decode a stringified token * @param token a token passed to the paginated request */const fromToken = <T>(token?: string): T | undefined => token ? (JSON.parse(Buffer.from(decodeURIComponent(token), 'base64').toString()) as T) : undefined;
/** * Encode pagination details into an opaque stringified token * @param paginationToken pagination token details */const toToken = <T>(paginationToken?: T): string | undefined => paginationToken ? encodeURIComponent(Buffer.from(JSON.stringify(paginationToken)).toString('base64')) : undefined;
Put Shopping List
import { randomUUID } from 'crypto';import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';import { putShoppingListHandler, PutShoppingListChainedHandlerFunction, INTERCEPTORS, Response, LoggingInterceptor,} from 'myapi-typescript-runtime';import { ddbClient } from './dynamo-client';
/** * Type-safe handler for the PutShoppingList operation */export const putShoppingList: PutShoppingListChainedHandlerFunction = async (request) => { LoggingInterceptor.getLogger(request).info('Start PutShoppingList Operation');
const shoppingListId = request.input.body.shoppingListId ?? randomUUID(); await ddbClient.send(new PutItemCommand({ TableName: 'shopping_list', Item: { shoppingListId: { S: shoppingListId, }, name: { S: request.input.body.name, }, shoppingItems: { S: JSON.stringify(request.input.body.shoppingItems || []), }, }, }));
return Response.success({ shoppingListId, });};
/** * Entry point for the AWS Lambda handler for the PutShoppingList operation. * The putShoppingListHandler method wraps the type-safe handler and manages marshalling inputs and outputs */export const handler = putShoppingListHandler(...INTERCEPTORS, putShoppingList);
import { randomUUID } from 'crypto';import { PutItemCommand } from '@aws-sdk/client-dynamodb';import { ddbClient } from './dynamo-client.js';import { PutShoppingList as PutShoppingListOperation } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js';
/** * Type-safe handler for the PutShoppingList operation */export const PutShoppingList: PutShoppingListOperation<ServiceContext> = async (input, ctx) => { ctx.logger.info('Start PutShoppingList Operation');
const shoppingListId = input.shoppingListId ?? randomUUID(); await ddbClient.send(new PutItemCommand({ TableName: 'shopping_list', Item: { shoppingListId: { S: shoppingListId, }, name: { S: input.name!, }, shoppingItems: { S: JSON.stringify(input.shoppingItems || []), }, }, }));
return { shoppingListId, };};
Smithy APIプロジェクトをapi
という名前で生成したのは、PDKプロジェクトとの一貫性を保つためpackages/api
に配置するためでした。現在のSmithy APIがservice Api
ではなくservice MyApi
を定義しているため、getApiServiceHandler
のインスタンスをすべてgetMyApiServiceHandler
に更新する必要があります。
handler.ts
を以下のように変更します:
import { getApiServiceHandler } from './generated/ssdk/index.js'; import { getMyApiServiceHandler } from './generated/ssdk/index.js';
process.env.POWERTOOLS_METRICS_NAMESPACE = 'Api';process.env.POWERTOOLS_SERVICE_NAME = 'Api';
const tracer = new Tracer();const logger = new Logger();const metrics = new Metrics();
const serviceHandler = getApiServiceHandler(Service); const serviceHandler = getMyApiServiceHandler(Service);
local-server.ts
も同様に変更します:
import { getApiServiceHandler } from './generated/ssdk/index.js';import { getMyApiServiceHandler } from './generated/ssdk/index.js';
const PORT = 3001;
const tracer = new Tracer();const logger = new Logger();const metrics = new Metrics();
const serviceHandler = getApiServiceHandler(Service);const serviceHandler = getMyApiServiceHandler(Service);
さらに、packages/api/backend/project.json
のmetadata.apiName
をmy-api
に更新します:
"metadata": { "generator": "ts#smithy-api", "apiName": "api", "apiName": "my-api", "auth": "IAM", "modelProject": "@shopping-list/api-model", "ports": [3001] },
ビルドによる検証
Section titled “ビルドによる検証”ここまでの移行が正常に完了したか確認するため、プロジェクトをビルドします:
pnpm nx run-many --target build
yarn nx run-many --target build
npx nx run-many --target build
bunx nx run-many --target build
ウェブサイトの移行
Section titled “ウェブサイトの移行”ショッピングリストアプリケーションで使用された CloudscapeReactTsWebsiteProject
は、CloudScape と Cognito 認証が組み込まれた React ウェブサイトを設定していました。
このプロジェクトタイプは非推奨となった create-react-app
を利用していました。このガイドでは、より現代的でサポートされた技術(具体的には [Vite](https://vite.dev/))を使用する ts#react-website
ジェネレータ を利用してウェブサイトの移行を行います。
移行の一環として、PDK で設定されていた React Router から TanStack Router に移行し、ルーティングの型安全性を強化します。
React ウェブサイトの生成
Section titled “React ウェブサイトの生成”ts#react-website
ジェネレータ を実行して、packages/website
にウェブサイトプロジェクトをセットアップします:
- インストール Nx Console VSCode Plugin まだインストールしていない場合
- VSCodeでNxコンソールを開く
- クリック
Generate (UI)
"Common Nx Commands"セクションで - 検索
@aws/nx-plugin - ts#react-website
- 必須パラメータを入力
- name: website
- クリック
Generate
pnpm nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive
yarn nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive
npx nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive
bunx nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive
変更されるファイルを確認するためにドライランを実行することもできます
pnpm nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive --dry-run
Cognito 認証の追加
Section titled “Cognito 認証の追加”上記の React ウェブサイトジェネレータは CloudscapeReactTsWebsiteProject
のようにデフォルトで Cognito 認証をバンドルしていないため、ts#react-website#auth
ジェネレータ を使用して明示的に追加します。
- インストール Nx Console VSCode Plugin まだインストールしていない場合
- VSCodeでNxコンソールを開く
- クリック
Generate (UI)
"Common Nx Commands"セクションで - 検索
@aws/nx-plugin - ts#react-website#auth
- 必須パラメータを入力
- project: website
- cognitoDomain: shopping-list
- クリック
Generate
pnpm nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive
yarn nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive
npx nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive
bunx nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive
変更されるファイルを確認するためにドライランを実行することもできます
pnpm nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-run
これにより、Cognito ホスト型 UI を使用したユーザーログインを確実に行うためのリダイレクトを管理する React コンポーネントが追加されます。また、packages/common/constructs
に UserIdentity
という名前の Cognito リソースをデプロイする CDK コンストラクトも追加されます。
ウェブサイトとAPIの接続
Section titled “ウェブサイトとAPIの接続”PDK では、プロジェクト間で相互に Projen プロジェクトを渡すことで統合コードを生成できました。ショッピングリストアプリケーションでは、この機能を使用してウェブサイトがAPIと連携できるように設定していました。
Nx Plugin for AWS では、api-connection
ジェネレータ を介してAPI統合をサポートしています。以下を使用して、ウェブサイトが Smithy API を呼び出せるようにします:
- インストール Nx Console VSCode Plugin まだインストールしていない場合
- VSCodeでNxコンソールを開く
- クリック
Generate (UI)
"Common Nx Commands"セクションで - 検索
@aws/nx-plugin - api-connection
- 必須パラメータを入力
- sourceProject: website
- targetProject: api
- クリック
Generate
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive
yarn nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive
npx nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive
bunx nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive
変更されるファイルを確認するためにドライランを実行することもできます
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive --dry-run
yarn nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive --dry-run
npx nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive --dry-run
bunx nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive --dry-run
これにより、生成された TypeScript クライアントを介してAPIを呼び出すために必要なクライアントプロバイダとビルドターゲットが生成されます。
AWS Northstar 依存関係の追加
Section titled “AWS Northstar 依存関係の追加”CloudscapeReactTsWebsiteProject
は自動的に @aws-northstar/ui
の依存関係を含んでいました。ショッピングリストアプリケーションで使用されているため、ここで追加します:
pnpm add -w @aws-northstar/ui
yarn add @aws-northstar/ui
npm install --legacy-peer-deps @aws-northstar/ui
bun install @aws-northstar/ui
コンポーネントとページの移動
Section titled “コンポーネントとページの移動”ショッピングリストアプリケーション には CreateItem
コンポーネントと、ShoppingList
、ShoppingLists
の2つのページがあります。これらを新しいウェブサイトに移行し、TanStack Router と Nx Plugin for AWS の TypeScript クライアントコードジェネレータを使用するために若干の調整を行います。
-
PDK プロジェクトから
packages/website/src/components/CreateItem/index.tsx
を新しいプロジェクトの同じ場所にコピー -
ShoppingLists
ページをホームページとして使用するため、packages/website/src/pages/ShoppingLists/index.tsx
をpackages/website/src/routes/index.tsx
にコピー(TanStack Router のファイルベースルーティングを使用) -
/:shoppingListId
ルートで表示するShoppingList
ページを、packages/website/src/pages/ShoppingList/index.tsx
からpackages/website/src/routes/$shoppingListId.tsx
にコピー
IDE にビルドエラーが表示されますが、以下の変更で新しいフレームワークに適合させます。
React Router から TanStack Router への移行
Section titled “React Router から TanStack Router への移行”ファイルベースルーティング を使用しているため、ローカル開発サーバーでルート設定の自動生成を管理できます。まずローカルサーバーを起動:
pnpm nx serve-local website
yarn nx serve-local website
npx nx serve-local website
bunx nx serve-local website
エラーが表示されますが、ローカルウェブサイトサーバーはポート 4200
で、Smithy API サーバーはポート 3001
で起動します。
以下の手順で routes/index.tsx
と routes/$shoppingListId.tsx
を TanStack Router に移行:
-
各ルートを登録する
createFileRoute
を追加:import { createFileRoute } from "@tanstack/react-router";...export default ShoppingLists;export const Route = createFileRoute('/')({component: ShoppingLists,});import { createFileRoute } from "@tanstack/react-router";...export default ShoppingList;export const Route = createFileRoute('/$shoppingListId')({component: ShoppingList,});ファイル保存後、
createFileRoute
の型エラーが解消されます。 -
useNavigate
フックの置き換え:インポートを更新:
import { useNavigate } from 'react-router-dom';import { useNavigate } from '@tanstack/react-router';navigate
メソッドの呼び出しを型安全なルート指定に変更:navigate(`/${cell.shoppingListId}`);navigate({to: '/$shoppingListId',params: { shoppingListId: cell.shoppingListId },}); -
useParams
フックの置き換え:インポートを削除:
import { useParams } from 'react-router-dom';useParams
の呼び出しをRoute
から取得するように変更(型安全):const { shoppingListId } = useParams();const { shoppingListId } = Route.useParams();
コンポーネントインポートの修正
Section titled “コンポーネントインポートの修正”ファイル構造の変更に伴い、CreateItem
のインポートパスを修正:
import CreateItem from "../../components/CreateItem";import CreateItem from "../components/CreateItem";
AppLayoutContext
の場所も変更:
import { AppLayoutContext } from "../../layouts/App";import { AppLayoutContext } from "../components/AppLayout";
新しい生成TypeScriptクライアントへの移行
Section titled “新しい生成TypeScriptクライアントへの移行”Nx Plugin for AWS が生成するTypeScriptクライアントを使用するため、以下の手順を実行:
-
新しいクライアントと型をインポート:
import {ShoppingList,usePutShoppingList,useDeleteShoppingList,useGetShoppingLists,} from "myapi-typescript-react-query-hooks";import { ShoppingList } from "../generated/my-api/types.gen";import { useMyApi } from "../hooks/useMyApi";import { useInfiniteQuery, useMutation } from "@tanstack/react-query"; -
TanStack Query フックを初期化:
const getShoppingLists = useGetShoppingLists({ pageSize: PAGE_SIZE });const putShoppingList = usePutShoppingList();const deleteShoppingList = useDeleteShoppingList();const api = useMyApi();const getShoppingLists = useInfiniteQuery(api.getShoppingLists.infiniteQueryOptions({ pageSize: PAGE_SIZE },{ getNextPageParam: (p) => p.nextToken },),);const putShoppingList = useMutation(api.putShoppingList.mutationOptions());const deleteShoppingList = useMutation(api.deleteShoppingList.mutationOptions(),); -
リクエストボディのラッパーを削除:
await putShoppingList.mutateAsync({putShoppingListRequestContent: {name: item,},});
TanStack Query v4 から v5 への移行
Section titled “TanStack Query v4 から v5 への移行”api-connection
ジェネレータが追加した TanStack Query v5 との差異を修正:
-
ミューテーションの
isLoading
をisPending
に置換:putShoppingList.isLoadingputShoppingList.isPending -
InfiniteQueryTable
の型エラーを抑制:<InfiniteQueryTablequery={getShoppingLists}query={getShoppingLists as any}
ローカルウェブサイトの確認
Section titled “ローカルウェブサイトの確認”http://localhost:4200/ にアクセスして動作確認できます。API、ウェブサイト、認証に加えて DynamoDB テーブル shopping_list
がリージョン内に存在し、ローカルAWS認証情報でアクセス可能な場合、完全に機能します。
インフラストラクチャの移行は次に行います。
インフラストラクチャの移行
Section titled “インフラストラクチャの移行”ショッピングリストアプリケーションで最後に移行が必要なプロジェクトが InfrastructureTsProject
です。これはTypeScript CDKプロジェクトであり、Nx Plugin for AWSの同等機能はts#infra
ジェネレータです。
PDKはProjenプロジェクトに加えて、これらのプロジェクトが依存するCDKコンストラクトも提供していました。私たちはこれらのCDKコンストラクトからもショッピングリストアプリケーションを移行し、Nx Plugin for AWSが生成するコンストラクトを採用します。
TypeScript CDKインフラストラクチャプロジェクトの生成
Section titled “TypeScript CDKインフラストラクチャプロジェクトの生成”packages/infra
にインフラプロジェクトをセットアップするため、ts#infra
ジェネレータを実行します:
- インストール Nx Console VSCode Plugin まだインストールしていない場合
- VSCodeでNxコンソールを開く
- クリック
Generate (UI)
"Common Nx Commands"セクションで - 検索
@aws/nx-plugin - ts#infra
- 必須パラメータを入力
- name: infra
- クリック
Generate
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
yarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
npx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
bunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
変更されるファイルを確認するためにドライランを実行することもできます
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
CDKインフラストラクチャの移行
Section titled “CDKインフラストラクチャの移行”PDKショッピングリストアプリケーションは、CDKアプリケーションスタック内で以下のコンストラクトをインスタンス化していました:
- ショッピングリストを保存するDynamoDBテーブルのための
DatabaseConstruct
- PDKから直接インポートしたCognitoリソース用の
UserIdentity
- Smithy APIをデプロイするための
MyApi
(PDKのTypeSafeRestApi
CDKコンストラクトを使用したタイプセーフな統合) - PDKの
StaticWebsite
CDKコンストラクトをラップしたウェブサイトデプロイ用のWebsite
次に、これらを新しいプロジェクトに移行します。
アプリケーションスタックのコピー
Section titled “アプリケーションスタックのコピー”PDKショッピングリストアプリケーションの packages/infra/src/stacks/application-stack.ts
を新しいプロジェクトの同じ場所にコピーします。TypeScriptエラーが表示されますが、後で対応します。
データベースコンストラクトのコピー
Section titled “データベースコンストラクトのコピー”PDKアプリケーションの packages/src/constructs/database.ts
にある Database
コンストラクトを新しいプロジェクトの同じ場所にコピーします。
Nx Plugin for AWSはPDK Nagよりも厳格なCheckovを使用するため、以下の抑制ルールを追加します:
import { suppressRules } from ':shopping-list/common-constructs';...suppressRules( this.shoppingListTable, ['CKV_AWS_28', 'CKV_AWS_119'], 'Backup and KMS key not required for this project',);
application-stack.ts
で DatabaseConstruct
のインポートをESM構文に更新します:
import { DatabaseConstruct } from '../constructs/database';import { DatabaseConstruct } from '../constructs/database.js';
UserIdentityコンストラクトの移行
Section titled “UserIdentityコンストラクトの移行”UserIdentity
コンストラクトは基本的にインポートを変更するだけで置換可能です:
import { UserIdentity } from "@aws/pdk/identity";import { UserIdentity } from ':shopping-list/common-constructs';...const userIdentity = new UserIdentity(this, `${id}UserIdentity`);
新しい UserIdentity
コンストラクトが使用する基盤リソースは aws-cdk-lib
から直接提供され、PDKでは @aws-cdk/aws-cognito-identitypool-alpha
を使用していました。
APIコンストラクトの移行
Section titled “APIコンストラクトの移行”PDKアプリケーションの constructs/apis/myapi.ts
には、Smithyモデルから生成されたタイプセーフなCDKコンストラクトをインスタンス化するコードがありました。
Nx Plugin for AWSもSmithyモデルに基づいたタイプセーフな統合を提供しますが、よりシンプルで柔軟な方法を採用しています。packages/common/constructs/src/app/apis/api.ts
が使用する最小限のメタデータを生成し、詳細はts#smithy-api
ガイドで確認できます。
以下の手順で移行します:
-
application-stack.ts
でApi
コンストラクトをインスタンス化stacks/application-stack.ts import { MyApi } from "../constructs/apis/myapi";import { Api } from ':shopping-list/common-constructs';...const myapi = new MyApi(this, "MyApi", {databaseConstruct,userIdentity,});const api = new Api(this, 'MyApi', {integrations: Api.defaultIntegrations(this).build(),});Api.defaultIntegrations(this).build()
はAPIの各操作に対してLambda関数を作成するデフォルト動作です。 -
Lambda関数にDynamoDBテーブルへのアクセス権限を付与
stacks/application-stack.ts databaseConstruct.shoppingListTable.grantReadData(api.integrations.getShoppingLists.handler,);[api.integrations.putShoppingList.handler,api.integrations.deleteShoppingList.handler,].forEach((f) => databaseConstruct.shoppingListTable.grantWriteData(f)); -
認証ユーザーにAPI呼び出し権限を付与
stacks/application-stack.ts api.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
ウェブサイトコンストラクトの移行
Section titled “ウェブサイトコンストラクトの移行”最後に、Website
コンストラクトを application-stack.ts
に追加します:
import { Website } from "../constructs/websites/website";import { Website } from ':shopping-list/common-constructs';...new Website(this, "Website", { userIdentity, myapi,});new Website(this, 'Website');
Nx Plugin for AWSのコンストラクトでは、ランタイム設定が各コンストラクト内で管理され、/runtime-config.json
に自動デプロイされます。
すべてのコードを移行したら、プロジェクトをビルドします:
pnpm nx run-many --target build
yarn nx run-many --target build
npx nx run-many --target build
bunx nx run-many --target build
コードベースの完全な移行が完了したので、デプロイ方法を検討できます。この時点で選択可能な2つのアプローチがあります。
新規リソースをすべて作成する(シンプルな方法)
Section titled “新規リソースをすべて作成する(シンプルな方法)”最もシンプルなアプローチは、これを完全に新しいアプリケーションとして扱うことです。つまり、新しいDynamoDBテーブルとCognitoユーザープールを「最初から作成」し、既存のユーザーとそのショッピングリストをすべて失います。この方法では、以下の手順を実行するだけです:
-
shopping_list
という名前のDynamoDBテーブルを削除 -
新しいアプリケーションをデプロイ:
Terminal window pnpm nx deploy infra shopping-list-infra-sandbox/*Terminal window yarn nx deploy infra shopping-list-infra-sandbox/*Terminal window npx nx deploy infra shopping-list-infra-sandbox/*Terminal window bunx nx deploy infra shopping-list-infra-sandbox/*
🎉 これで完了です! 🎉
既存のステートフルリソースをダウンタイムなしで移行(より複雑な方法)
Section titled “既存のステートフルリソースをダウンタイムなしで移行(より複雑な方法)”実際には、既存のAWSリソースを新しいコードベースで管理できるように移行しつつ、顧客へのサービス停止を回避したい場合がほとんどでしょう。
ショッピングリストアプリケーションにおいて重要なステートフルリソースは、ユーザーのショッピングリストを含むDynamoDBテーブルと、登録ユーザーの詳細を含むユーザープールです。高レベルの計画としては、これら2つの主要リソースを保持し、新しいスタックで管理されるように移動した後、DNSを更新して新しいWebサイト(および顧客に公開されている場合はAPI)を指すようにします。
-
保持したい既存リソースを参照するように新しいアプリケーションを更新
ショッピングリストアプリケーションでは、DynamoDBテーブルに対して以下を実行:
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);Cognitoユーザープールに対しては:
packages/common/constructs/src/core/user-identity.ts this.userPool = this.createUserPool();this.userPool = UserPool.fromUserPoolId(this,'UserPool','<your-user-pool-id>',); -
新しいアプリケーションをビルドしてデプロイ:
Terminal window pnpm nx run-many --target buildTerminal window yarn nx run-many --target buildTerminal window npx nx run-many --target buildTerminal window bunx nx run-many --target buildTerminal window pnpm nx deploy infra shopping-list-infra-sandbox/*Terminal window yarn nx deploy infra shopping-list-infra-sandbox/*Terminal window npx nx deploy infra shopping-list-infra-sandbox/*Terminal window bunx nx deploy infra shopping-list-infra-sandbox/*これで既存リソースを参照する新しいアプリケーションが起動しましたが、まだトラフィックは受けていません。
-
完全な結合テストを実施し、新しいアプリケーションが期待通りに動作することを確認。ショッピングリストアプリケーションでは、Webサイトをロードしてサインイン、ショッピングリストの作成/表示/編集/削除が可能か確認。
-
既存リソースを参照する変更を新しいアプリケーションで元に戻す(ただしまだデプロイしない):
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);Cognitoユーザープールに対しては:
packages/common/constructs/src/core/user-identity.ts this.userPool = this.createUserPool();this.userPool = UserPool.fromUserPoolId(this,'UserPool','<your-user-pool-id>',);その後ビルドを実行:
Terminal window pnpm nx run-many --target buildTerminal window yarn nx run-many --target buildTerminal window npx nx run-many --target buildTerminal window bunx nx run-many --target build -
新しいアプリケーションの
packages/infra
フォルダでcdk import
を実行し、インポート対象リソースを確認:New Application cd packages/infrapnpm exec cdk import shopping-list-infra-sandbox/Application --forceEnterキーを押してプロンプトを進めてください。リソースが別スタックで管理されているためインポートは失敗します(これは予期された動作です)。インポート対象リソースを確認するためだけのステップです。以下のような出力が表示されます:
Terminal window shopping-list-infra-sandbox/Application/ApplicationUserIdentity/UserPool/smsRole/Resource (AWS::IAM::Role): enter RoleName (empty to skip)shopping-list-infra-sandbox/Application/ApplicationUserIdentity/UserPool/Resource (AWS::Cognito::UserPool): enter UserPoolId (empty to skip)shopping-list-infra-sandbox/Application/Database/ShoppingList/Resource (AWS::DynamoDB::Table): import with TableName=shopping_list (y/n) yこれにより、実際には3つのリソースを新しいスタックにインポートする必要があることがわかります。
-
前のステップで発見したリソースに対して、古いPDKプロジェクトで
RemovalPolicy
をRETAIN
に設定:application-stack.ts const userIdentity = new UserIdentity(this, `${id}UserIdentity`, {userPool,});const smsRole = userIdentity.userPool.node.findAll().filter(c => CfnResource.isCfnResource(c) &&c.node.path.includes('/smsRole/'))[0] as CfnResource;smsRole.applyRemovalPolicy(RemovalPolicy.RETAIN); -
削除ポリシーを適用するためPDKプロジェクトをデプロイ:
PDK Application cd packages/infranpx projen deploy -
CloudFormationコンソールで
cdk import
ステップで要求された値を記録:- ユーザープールID(例:
us-west-2_XXXXX
) - SMSロール名(例:
infra-sandbox-UserIdentityUserPoolsmsRoleXXXXXX
)
- ユーザープールID(例:
-
リソースを作成する代わりに既存リソースを参照するようPDKプロジェクトを更新:
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);Cognitoユーザープールに対しては:
application-stack.ts const userPool = UserPool.fromUserPoolId(this,'UserPool','<your-user-pool-id>',);const userIdentity = new UserIdentity(this, `${id}UserIdentity`, {// PDKコンストラクトはIUserPoolではなくUserPoolを受け取りますが、問題なく動作します!userPool: userPool as any,}); -
PDKプロジェクトを再度デプロイ(これによりリソースがPDKプロジェクトのCloudFormationスタックで管理されなくなります):
PDK Application cd packages/infranpx projen deploy -
リソースが管理対象外になったら、新しいアプリケーションで
cdk import
を実行して実際にインポート:New Application cd packages/infrapnpm exec cdk import shopping-list-infra-sandbox/Application --forceプロンプトで値を入力すると、インポートが正常に完了します。
-
既存リソース(現在は新しいスタックで管理)への変更を適用するため、新しいアプリケーションを再度デプロイ:
Terminal window pnpm nx deploy infra shopping-list-infra-sandbox/*Terminal window yarn nx deploy infra shopping-list-infra-sandbox/*Terminal window npx nx deploy infra shopping-list-infra-sandbox/*Terminal window bunx nx deploy infra shopping-list-infra-sandbox/* -
新しいアプリケーションの完全テストを再度実施
-
新しいWebサイト(および必要に応じてAPI)を指すようDNSレコードを更新:
Route53のWeighted Routingを使用した段階的な移行を推奨します。最初は一部のリクエストを新しいアプリケーションに転送し、メトリクスを監視しながら徐々に重みを増やしていき、最終的に古いPDKアプリケーションへのトラフィックをゼロにします。
DNSがなくWebサイト/APIに自動生成ドメインを使用している場合は、CloudFront HTTP originやAPI Gateway HTTP integrationを使用してリクエストをプロキシできます。
-
PDKアプリケーションのメトリクスを監視してトラフィックがないことを確認後、古いCloudFormationスタックを削除:
Terminal window cd packages/infranpx projen destroy
かなり手間がかかりましたが、ユーザーをシームレスに新しいアプリケーションに移行できました! 🎉🎉🎉
これで、PDKよりもNx Plugin for AWSの新しいメリットを享受できます:
- より高速なビルド
- ローカルAPI開発のサポート
- 快適なコーディングが可能なコードベース(MCPサーバーを試してみてください!)
- より直感的なタイプセーフなクライアント/サーバーコード
- その他多数!
よくある質問
Section titled “よくある質問”このセクションでは、上記の移行例でカバーされていないPDK機能に関するガイダンスを提供します。
一般的なルールとして、PDKからの移行時にはNxワークスペースからプロジェクトを開始することを推奨します(PDK Monorepoとの類似性を考慮)。また、新しいタイプを構築する際の基盤として、当プラグインのジェネレータを使用することをお勧めします。
npx create-nx-workspace@21.4.1 my-project --pm=pnpm --preset=@aws/nx-plugin --ci=skip
npx create-nx-workspace@21.4.1 my-project --pm=yarn --preset=@aws/nx-plugin --ci=skip
npx create-nx-workspace@21.4.1 my-project --pm=npm --preset=@aws/nx-plugin --ci=skip
npx create-nx-workspace@21.4.1 my-project --pm=bun --preset=@aws/nx-plugin --ci=skip
CDK Graph
Section titled “CDK Graph”CDK Graphは接続されたCDKリソースのグラフを構築し、2つのプラグインを提供します:
Diagram Plugin
Section titled “Diagram Plugin”CDK Graph Diagram PluginはCDKインフラストラクチャからAWSアーキテクチャ図を生成します。
同様の決定論的アプローチとして有効な代替手段はCDK-Diaがあります。
生成AIの進化に伴い、多くの基盤モデルがCDKインフラストラクチャから高品質な図を作成可能です。AWS Diagram MCP Serverの使用をお試しください。チュートリアルについてはこちらのブログ記事を参照してください。
Threat Composer Plugin
Section titled “Threat Composer Plugin”CDK Graph Threat Composer PluginはCDKコードからThreat Composerのスターター脅威モデルを生成します。
このプラグインは、サンプルの脅威を含むベース脅威モデルをフィルタリングし、スタックで使用するリソースに基づいて絞り込むことで機能します。
これらの具体的なサンプル脅威に興味がある場合は、ベース脅威モデルをコピーしてフィルタリングするか、基盤モデルが類似モデルを生成する際のコンテキストとして使用できます。
AWS Arch
Section titled “AWS Arch”AWS Arch は、CDK Graph向けにCloudFormationリソースと関連するアーキテクチャアイコンのマッピングを提供しています。
アイコン関連のリソースについては AWS Architecture Icons page を参照してください。Diagrams はコードとしてダイアグラムを構築する方法も提供しています。
直接使用する場合は、プロジェクトをフォークしてオーナーシップを取得することを検討してください!
パイプライン
Section titled “パイプライン”PDKはPDKPipelineProject
を提供しており、CDKインフラストラクチャプロジェクトをセットアップし、CDK PipelinesリソースをラップしたCDKコンストラクトを利用していました。
これから移行する場合、CDK Pipelinesコンストラクトを直接使用できます。ただし実際には、CDK Stagesを定義し、適切なステージのデプロイコマンドを直接実行するGitHub ActionsやGitLab CI/CDのようなツールを使用する方がより直接的である可能性が高いでしょう。
PDK Nag
Section titled “PDK Nag”PDK NagはCDK Nagをラップし、プロトタイプ構築向けの特定ルールセットを提供します。
PDK Nagからの移行にはCDK Nagを直接使用してください。同じルールセットが必要な場合、こちらのドキュメントに従って独自の「パック」を作成できます。
Type Safe API
Section titled “Type Safe API”上記の移行例ではType Safe APIで最も一般的に使用されるコンポーネントをカバーしていますが、他の機能に関する移行の詳細は以下に記載します。
OpenAPIでモデル化されたAPI
Section titled “OpenAPIでモデル化されたAPI”Nx Plugin for AWSはSmithyでモデル化されたAPIをサポートしていますが、直接OpenAPIでモデル化されたAPIはサポートしていません。ts#smithy-api
ジェネレーターはカスタマイズ可能な良い出発点となります。Smithyの代わりにOpenAPI仕様をmodel
プロジェクトのsrc
フォルダーに定義し、クライアント/サーバーのコード生成ツールがNPMで利用できない場合、build.Dockerfile
を修正して目的のツールを使用できます。目的のツールがNPMで利用可能な場合、Nxワークスペースに開発依存関係としてインストールし、Nxビルドターゲットから直接呼び出すことができます。
バックエンド
Section titled “バックエンド”OpenAPIでモデル化された型安全なバックエンドには、OpenAPI Generatorのサーバージェネレーターの使用を検討できます。これらはAWS Lambda向けに直接生成しませんが、AWS Lambda Web Adapterを使用して多くのケースでギャップを埋めることができます。
クライアント
Section titled “クライアント”TypeScriptクライアントの場合、ts#react-website
ジェネレーターとapi-connection
ジェネレーターをts#smithy-api
の例と組み合わせて使用し、クライアントの生成とウェブサイトへの統合方法を確認できます。これはopen-api#ts-client
またはopen-api#ts-hooks
ジェネレーターを呼び出すビルドターゲットを設定します。これらのジェネレーターをOpenAPI仕様を指すように自分で使用できます。
その他の言語の場合、OpenAPI Generatorのジェネレーターが要件に合うか確認できます。
ts#nx-generator
ジェネレーターを使用してカスタムジェネレーターを構築することも可能です。OpenAPIからのコード生成方法の詳細については、該当ジェネレーターのドキュメントを参照してください。Nx Plugin for AWSのテンプレートを出発点として使用できます。PDKコードベースのテンプレートも参考にできますが、テンプレートが操作するデータ構造がNx Plugin for AWSとは若干異なる点に注意してください。
TypeSpecでモデル化されたAPI
Section titled “TypeSpecでモデル化されたAPI”TypeSpecの場合、上記のOpenAPIに関するセクションが同様に適用されます。ts#smithy-api
を生成し、TypeSpecコンパイラーとOpenAPIパッケージをNxワークスペースにインストールした後、モデルプロジェクトのcompile
ターゲットをtsp compile
を実行するように更新し、OpenAPI仕様をdist
ディレクトリに出力するようにします。
バックエンド
Section titled “バックエンド”推奨アプローチは、TypeSpec HTTP Server generator for JavaScriptを使用してサーバーコードを生成することです。これはTypeSpecモデルを直接操作します。
AWS Lambdaでの実行にはAWS Lambda Web Adapterを使用できます。
上記のOpenAPIオプションも使用可能です。
クライアント
Section titled “クライアント”TypeSpecはType Safe APIがサポートする3言語すべてでクライアント生成機能を備えています:
TypeSpecはOpenAPIにコンパイル可能なため、上記のOpenAPIセクションも適用されます。
Smithyでモデル化されたAPI
Section titled “Smithyでモデル化されたAPI”上記の移行例ではts#smithy-api
ジェネレーターへの移行の概要を説明しています。このセクションではPythonとJavaのバックエンドおよびクライアントのオプションについて説明します。
バックエンド
Section titled “バックエンド”Smithy JavaコードジェネレーターはJavaサーバージェネレーターとAWS Lambda用アダプターを提供します。
SmithyはPython用サーバージェネレーターを持たないため、OpenAPI経由で対応する必要があります。Pythonバックエンドのオプションについては、上記のOpenAPIでモデル化されたAPIセクションを参照してください。
クライアント
Section titled “クライアント”Smithy JavaコードジェネレーターはJavaクライアントジェネレーターを提供します。
PythonクライアントにはSmithy Pythonを確認できます。
TypeScriptにはSmithy TypeScriptを使用するか、ts#smithy-api
で採用したOpenAPI経由のアプローチ(tRPC、FastAPI、Smithy API間で一貫性を保つためTanStack Queryフックを使用)を適用できます。
Smithy Shapeライブラリ
Section titled “Smithy Shapeライブラリ”Type Safe APIはSmithyShapeLibraryProject
というProjenプロジェクトタイプを提供し、複数のSmithyベースAPIで再利用可能なSmithyモデルを含むプロジェクトを設定しました。
これを実現する最も簡単な方法は以下の通りです:
Shapeライブラリの作成
Section titled “Shapeライブラリの作成”-
smithy#project
ジェネレーターでshapeライブラリを作成:- インストール Nx Console VSCode Plugin まだインストールしていない場合
- VSCodeでNxコンソールを開く
- クリック
Generate (UI)
"Common Nx Commands"セクションで - 検索
@aws/nx-plugin - smithy#project
- 必須パラメータを入力
- クリック
Generate
Terminal window pnpm nx g @aws/nx-plugin:smithy#projectTerminal window yarn nx g @aws/nx-plugin:smithy#projectTerminal window npx nx g @aws/nx-plugin:smithy#projectTerminal window bunx nx g @aws/nx-plugin:smithy#project変更されるファイルを確認するためにドライランを実行することもできます
Terminal window pnpm nx g @aws/nx-plugin:smithy#project --dry-runTerminal window yarn nx g @aws/nx-plugin:smithy#project --dry-runTerminal window npx nx g @aws/nx-plugin:smithy#project --dry-runTerminal window bunx nx g @aws/nx-plugin:smithy#project --dry-runserviceName
オプションには任意の名前を指定(後でservice
shapeを削除します) -
src
のデフォルトモデルを必要なshape定義に置き換え -
smithy-build.json
を更新してplugins
と未使用のMaven依存関係を削除 -
build.Dockerfile
を最小限のビルドステップに置き換え:build.Dockerfile FROM public.ecr.aws/docker/library/node:24 AS builder# 出力ディレクトリRUN mkdir /out# Smithy CLIのインストール# https://smithy.io/2.0/guides/smithy-cli/cli_installation.htmlWORKDIR /smithyARG TARGETPLATFORMRUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then ARCH="aarch64"; else ARCH="x86_64"; fi && \mkdir -p smithy-install/smithy && \curl -L https://github.com/smithy-lang/smithy/releases/download/1.61.0/smithy-cli-linux-$ARCH.zip -o smithy-install/smithy-cli-linux-$ARCH.zip && \unzip -qo smithy-install/smithy-cli-linux-$ARCH.zip -d smithy-install && \mv smithy-install/smithy-cli-linux-$ARCH/* smithy-install/smithyRUN smithy-install/smithy/install# プロジェクトファイルのコピーCOPY smithy-build.json .COPY src src# MavenキャッシュマウントでのSmithyビルドRUN --mount=type=cache,target=/root/.m2/repository,id=maven-cache \smithy buildRUN cp -r build/* /out/# /outディレクトリのエクスポートFROM scratch AS exportCOPY --from=builder /out /
Shapeライブラリの利用
Section titled “Shapeライブラリの利用”サービスモデルプロジェクトで以下の変更を行いshapeライブラリを利用:
-
project.json
のcompile
ターゲットを更新し、ワークスペースをビルドコンテキストとして追加し、shapeライブラリのbuild
ターゲットに依存:project.json {"cache": true,"outputs": ["{workspaceRoot}/dist/{projectRoot}/build"],"executor": "nx:run-commands","options": {"commands": ["rimraf dist/packages/api/model/build","make-dir dist/packages/api/model/build","docker build --build-context workspace=. -f packages/api/model/build.Dockerfile --target export --output type=local,dest=dist/packages/api/model/build packages/api/model"],"parallel": false,"cwd": "{workspaceRoot}"},"dependsOn": ["@my-project/shapes:build"]} -
build.Dockerfile
を更新してshapeライブラリのsrc
ディレクトリをコピー(例:shapeライブラリがpackages/shapes
にある場合):build.Dockerfile # プロジェクトファイルのコピーCOPY smithy-build.json .COPY src srcCOPY --from=workspace packages/shapes/src shapes -
smithy-build.json
を更新してsourcesにshapesディレクトリを追加:smithy-build.json {"version": "1.0","sources": ["src/", "shapes/"],"plugins": {...}
インターセプター
Section titled “インターセプター”Type Safe APIは以下のデフォルトインターセプターを提供:
- Powertools for AWS Lambdaを使用したロギング、トレーシング、メトリクス
- 未捕捉例外処理用try-catchインターセプター
- CORSヘッダー返信用インターセプター
ts#smithy-api
ジェネレーターはMiddyを使用してPowertools for AWS Lambdaによるロギング、トレーシング、メトリクスを計装します。try-catchインターセプターの動作はSmithy TypeScript SSDKに組み込まれており、CORSヘッダーはhandler.ts
で追加されます。
任意の言語でのロギング、トレーシング、メトリクスインターセプターにはPowertools for AWS Lambdaを直接使用します。
カスタムインターセプターの移行には以下を推奨:
- TypeScript - Middy
- Python - Powertools Middleware Factory
- Java - aws-lambda-java-libsを使用したシンプルなアプローチ、またはAspectJによるアノテーションベースのミドルウェア構築
ドキュメント生成
Section titled “ドキュメント生成”Type Safe APIはRedocly CLIを使用したドキュメント生成を提供しました。これは上記の移行後に既存プロジェクトに簡単に追加できます。
-
Redocly CLIをインストール:
Terminal window pnpm add -Dw @redocly/cliTerminal window yarn add -D @redocly/cliTerminal window npm install --legacy-peer-deps -D @redocly/cliTerminal window bun install -D @redocly/cli -
redocly build-docs
を使用してモデルプロジェクトにドキュメント生成ターゲットを追加:model/project.json {..."documentation": {"cache": true,"outputs": ["{workspaceRoot}/dist/{projectRoot}/documentation"],"executor": "nx:run-commands","options": {"command": "redocly build-docs dist/packages/api/model/build/openapi/openapi.json --output=dist/packages/api/model/documentation/index.html","cwd": "{workspaceRoot}"},"dependsOn": ["compile"]}}
OpenAPI Generatorのドキュメントジェネレーターも検討可能です。
Type Safe APIは生成されたインフラストラクチャパッケージ内にモックを生成しました。
JSON Schema Fakerに移行可能です。これはOpenAPI仕様に基づいてモックデータを作成でき、CLIをmodel
プロジェクトのビルドの一部として実行できます。
CDKインフラストラクチャを更新してJSON Schema Fakerが生成したJSONファイルを読み込み、適切なAPI GatewayMockIntegration
を返すようにします(ts#smithy-api
ジェネレーターを使用した場合のmetadata.gen.ts
をベースに)。
混合言語バックエンド
Section titled “混合言語バックエンド”Type Safe APIは複数言語でのAPI実装をサポートしていました。これはCDKでAPIコンストラクトをインスタンス化する際に統合をオーバーライドすることで実現可能です:
const pythonLambdaHandler = new Function(this, 'PythonImplementation', { runtime: Runtime.PYTHON_3_12, ...});
new MyApi(this, 'MyApi', { integrations: Api.defaultIntegrations(this) .withOverrides({ echo: { integration: new LambdaIntegration(pythonLambdaHandler), handler: pythonLambdaHandler, }, }) .build(),});
ts#smithy-api
とTypeScript Server SDKを使用する場合、サービス/ルーターのスタブを作成する必要があります:
export const Service: ApiService<ServiceContext> = { ... Echo: () => { throw new Error(`Not Implemented`); },};
入力バリデーション
Section titled “入力バリデーション”Type Safe APIはSpecRestApi
コンストラクトを使用して、OpenAPI仕様に基づくAPI Gatewayのネイティブリクエストボディバリデーションを実装していました。
ts#smithy-api
ジェネレーターでは、バリデーションはServer SDK自体で実行されます。これはほとんどのサーバージェネレーターで同様です。
ネイティブのAPI Gatewayバリデーションを実装するには、packages/common/constructs/src/core/api/rest-api.ts
を修正して、各操作のリクエストボディに関連するJSONスキーマをOpenAPI仕様から読み取るようにします。
WebSocket API
Section titled “WebSocket API”Type Safe APIのWebSocket API(API GatewayとLambdaを使用したモデル駆動開発)の直接的な移行パスは残念ながらありませんが、いくつかのアイデアを提示します。
非同期API向けに設計されたAsyncAPIの使用を検討できます。AsyncAPI NodeJSテンプレートで生成したNode WebSocketバックエンドをECSでホストできます。
インフラストラクチャにはAppSync EventsとPowertoolsの使用を検討できます。関連ブログ記事が参考になります。
AppSyncのWebSocket対応GraphQL APIも選択肢です。GitHubイシューで要望表明可能です。AppSync開発者ガイドとサンプルプロジェクトを参照してください。
Type Safe APIと同じベンダー拡張を解釈するカスタムコードジェネレーターの作成も可能です。OpenAPIベースのカスタムジェネレーター構築についてはOpenAPIでモデル化されたAPIセクションを参照。API Gateway WebSocket API Lambdaハンドラーテンプレートとクライアントテンプレートを参考にできます。
ts#trpc-api
ジェネレーターへの移行も検討可能です(現時点ではサブスクリプション/ストリーミング未サポート)。関連GitHubイシューで要望表明可能です。
Smithyはプロトコル非依存ですが、WebSocketプロトコルのサポートは未実装です。GitHubイシューを参照してください。
Python/Javaでのインフラストラクチャ
Section titled “Python/Javaでのインフラストラクチャ”PDKがサポートするPythonおよびJavaで記述されたCDKインフラストラクチャ。現時点ではNx Plugin for AWSではこれをサポートしていません。
推奨される解決策としては、CDKインフラストラクチャをTypeScriptに移行するか、当社のジェネレータを使用して共通コンストラクトパッケージを目的の言語に移行する方法があります。Amazon Q CLIなどのGenerative AIを活用して、合成されたCloudFormationテンプレートが同一になるまでAIエージェントに移作業を反復させることが可能です。
この原則はPythonやJavaで生成されるType Safe APIのインフラストラクチャにも適用されます。共通コンストラクトパッケージから汎用的なrest-api.ts
コンストラクトを翻訳し、対象言語向けのシンプルなメタデータジェネレータを独自に実装できます(OpenAPIでモデル化されたAPIセクション参照)。
Pythonプロジェクトのベースとしてpy#project
ジェネレータを使用し、CDKコードを追加できます(cdk.json
ファイルを移行し、関連ターゲットを追加)。JavaプロジェクトにはNxの@nx/gradle
プラグインを、Mavenには@jnxplus/nx-maven
を使用できます。
Projenの使用
Section titled “Projenの使用”PDKはProjenを基盤として構築されました。ProjenとNxジェネレータには根本的な違いがあるため、技術的には組み合わせることが可能ではあるものの、それはアンチパターンとなる可能性が高いです。Projenはプロジェクトファイルをコードとして管理するため直接編集ができませんが、Nxジェネレータはプロジェクトファイルを一度生成した後は自由にコードを変更できます。
Projenの使用を継続したい場合、独自に目的のProjenプロジェクトタイプを実装できます。Nx Plugin for AWSのパターンに従うには、当社のジェネレータを実行するかGitHub上のソースコードを確認して目的のプロジェクトタイプがどのように構築されているかを把握し、Projenのプリミティブを使用して関連部分を実装することが可能です。