このガイドでは、AWS PDK プロジェクトをNx Plugin for AWSに移行する例を説明し、このトピックに関する一般的なガイダンスを提供します。
Nx Plugin for AWSへの移行により、PDKと比較して以下のメリットが得られます:
より高速なビルド
より使いやすい(UIとCLI)
Vibe-codingフレンドリー(MCPサーバーをお試しください! )
より最新のテクノロジー
ローカルAPIとウェブサイト開発
より多くの制御(ユースケースに合わせて提供されたファイルを変更可能)
その他多数!
ステップバイステップガイドではありません
プロジェクトを移行する際には、多くのニュアンスと考慮事項があります。このガイドは簡単な例を対象としており、お客様のプロジェクトに必要な手順が欠けている可能性があります。
このガイドでは、新しいワークスペースを作成し、PDKプロジェクトから新しいプロジェクトに部品をコピーするアプローチを採用しています。異なる方法で移行する必要がある場合、またはPDKからNx Plugin for AWSへの移行に関する質問がある場合は、GitHubディスカッションでお問い合わせください !
また、PDKの一部の機能には正確な同等物がないことにも注意してください。これらのヒント/アイデアについては、FAQ セクションを参照してください。
このガイドでは、PDKチュートリアルのショッピングリストアプリケーション を移行対象プロジェクトとして使用します。ご自身で実際に試す場合は、そのチュートリアルの手順に従って対象プロジェクトを作成してください。
ショッピングリストアプリケーションは、以下のPDKプロジェクトタイプで構成されています:
MonorepoTsProject
TypeSafeApiProject
CloudscapeReactTsWebsiteProject
InfrastructureTsProject
まず、新しいプロジェクト用の新しいワークスペースを作成します。インプレース移行よりも極端ですが、このアプローチにより最もクリーンな最終結果が得られます。Nxワークスペースの作成は、PDKのMonorepoTsProjectの使用と同等です:
ワークスペースを作成 @aws/nx-workspace@1.0.0
$ pnpm create @aws/nx-workspace@1.0.0 shopping-list --iac= cdk $ yarn create @aws/nx-workspace@1.0.0 shopping-list --iac= cdk $ npm create @aws/nx-workspace@1.0.0 -- shopping-list --iac= cdk $ bun create @aws/nx-workspace@1.0.0 shopping-list --iac= cdk
このコマンドで作成されたshopping-listディレクトリをお気に入りのIDEで開きます。
ショッピングリストアプリケーションで使用されている TypeSafeApiProject は、以下を利用していました:
モデリング言語として Smithy
オペレーションの実装にTypeScript
Reactウェブサイトとの統合のためのTypeScriptフック生成
したがって、ts#smithy-api ジェネレーター を使用して同等の機能を提供できます。
ts#api ジェネレーター を framework を smithy に設定して実行し、packages/api にAPIプロジェクトをセットアップします:
このジェネレーターを実行 @aws/nx-plugin:ts#api
$ pnpm nx g @aws/nx-plugin:ts#api --name= api --framework= smithy --namespace= com.aws --auth= iam --no-interactive $ yarn nx g @aws/nx-plugin:ts#api --name= api --framework= smithy --namespace= com.aws --auth= iam --no-interactive $ npx nx g @aws/nx-plugin:ts#api --name= api --framework= smithy --namespace= com.aws --auth= iam --no-interactive $ bunx nx g @aws/nx-plugin:ts#api --name= api --framework= smithy --namespace= com.aws --auth= iam --no-interactive インストール Nx Console VSCode Plugin まだインストールしていない場合 VSCodeでNxコンソールを開く クリック Generate (UI) "Common Nx Commands"セクションで 検索 @aws/nx-plugin - ts#api 必須パラメータを入力name: apiframework: smithynamespace: com.awsauth: iam クリック Generate
これにより、model プロジェクトと backend プロジェクトが生成されることがわかります。model プロジェクトにはSmithyモデルが含まれ、backend にはサーバー実装が含まれます。
バックエンドは Smithy Server Generator for TypeScript を使用します。これについては以下でさらに詳しく説明します。
Smithy APIプロジェクトの基本構造ができたので、モデルを移行できます:
packages/api/model/src にある生成されたサンプルSmithyファイルを削除します
PDKプロジェクトの packages/api/model/src/main/smithy ディレクトリからモデルを新しいプロジェクトの packages/api/model/src ディレクトリにコピーします。
smithy-build.json のサービス名と名前空間をPDKアプリケーションに合わせて更新します:
" service " : "com.aws#MyApi" ,
main.smithy のサービスを更新して ValidationException エラーを追加します。これはSmithy TypeScript Server SDKを使用する際に必要です。
use smithy.framework#ValidationException
packages/api/model/src に extensions.smithy ファイルを追加し、生成されたクライアントにページネーション情報を提供するトレイトを定義します:
use smithy.openapi#specificationExtension
@ specificationExtension ( as : "x-cursor" )
get-shopping-lists.smithy の GetShoppingLists オペレーションに新しい @cursor トレイトを追加します:
@ 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)でいくつかの失敗が発生しますが、次にそれらに対処します。
api/backend プロジェクトは、Type Safe APIの api/handlers/typescript プロジェクトとある程度同等と考えることができます。
Type Safe APIと ts#smithy-api ジェネレーターの主な違いの1つは、ハンドラーがType Safe API独自の生成されたハンドラーラッパー(api/generated/typescript/runtime プロジェクトにあります)ではなく、Smithy Server Generator for TypeScript を使用して実装されることです。
ショッピングリストアプリケーションのLambdaハンドラーは @aws-sdk/client-dynamodb パッケージに依存しているため、@shopping-list/api プロジェクトにインストールしましょう:
pnpm add @aws-sdk/client-dynamodb --filter api
yarn workspace @shopping-list/api add @aws-sdk/client-dynamodb
npm install --legacy-peer-deps @aws-sdk/client-dynamodb -w packages/api
bun add @aws-sdk/client-dynamodb --cwd packages/api
次に、PDKプロジェクトから handlers/typescript/src/dynamo-client.ts ファイルを backend/src/operations にコピーして、ハンドラーで使用できるようにします。
ts#smithy-api ジェネレーターは、サンプルの Echo オペレーションをスキャフォールドします。モデルからこれを削除したので、backend/src/operations/echo.ts の対応するハンドラーを削除します。移行したオペレーションは、以下の service.ts で登録します。
ハンドラーを移行するには、次の一般的な手順に従います:
PDKプロジェクトの packages/api/handlers/typescript/src ディレクトリから新しいプロジェクトの packages/api/backend/src/operations ディレクトリにハンドラーをコピーします。
my-api-typescript-runtime のインポートを削除し、代わりに生成されたTypeScript Server SDKからオペレーションタイプと ServiceContext をインポートします。例:
deleteShoppingListHandler ,
DeleteShoppingListChainedHandlerFunction ,
} from 'myapi-typescript-runtime' ;
import { DeleteShoppingList as DeleteShoppingListOperation } from '../generated/ssdk/index.js' ;
import { ServiceContext } from '../context.js' ;
ハンドラーラッパーのエクスポートを削除します
export const handler = deleteShoppingListHandler (
オペレーションハンドラーのシグネチャをSSDKを使用するように更新します:
export const deleteShoppingList : DeleteShoppingListChainedHandlerFunction = async ( request ) => {
export const DeleteShoppingList : DeleteShoppingListOperation < ServiceContext > = async ( input , ctx ) => {
LoggingInterceptor の使用を ctx.logger に置き換えます。(メトリクスとトレーシングのインターセプターにも適用されます):
LoggingInterceptor . getLogger ( request ). 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' });
すべてのインポートをESM構文を使用するように更新します。つまり、相対インポートに .js 拡張子を追加します。
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' ;
// Register operations to the service here
export const Service : MyApiService < ServiceContext > = {
チュートリアルの3つのショッピングリストオペレーションの完全な移行前後の例については、ここをクリックしてください
Delete Shopping List import { DeleteItemCommand } from '@aws-sdk/client-dynamodb' ;
deleteShoppingListHandler ,
DeleteShoppingListChainedHandlerFunction ,
} 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 ;
TableName : 'shopping_list' ,
return Response . success ({
* 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 (
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 ) => {
'Start DeleteShoppingList Operation' ,
const shoppingListId = input . shoppingListId ;
TableName : 'shopping_list' ,
Get Shopping Lists import { DynamoDBClient , QueryCommand , QueryCommandInput , ScanCommand , ScanCommandInput } from '@aws-sdk/client-dynamodb' ;
GetShoppingListsChainedHandlerFunction ,
} 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' ,
ExclusiveStartKey : nextToken ? fromToken ( nextToken ) : undefined ,
KeyConditionExpression : 'shoppingListId = :shoppingListId' ,
ExpressionAttributeValues : {
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 !,
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' ,
ExclusiveStartKey : nextToken ? fromToken ( nextToken ) : undefined ,
KeyConditionExpression : 'shoppingListId = :shoppingListId' ,
ExpressionAttributeValues : {
S : input . shoppingListId !,
const response = await ddbClient . send ( shoppingListId ? new QueryCommand ( commandInput ) : new ScanCommand ( commandInput ));
shoppingLists : ( response . Items || [])
. map < ShoppingList >( item => ({
shoppingListId : item . shoppingListId . 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' ;
PutShoppingListChainedHandlerFunction ,
} 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' ,
S : request . input . body . name ,
S : JSON . stringify ( request . input . body . shoppingItems || []),
return Response . success ({
* 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' ,
S : JSON . stringify ( input . shoppingItems || []),
最初に api という名前でSmithy 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 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 に更新します:
" generator " : "ts#smithy-api" ,
" modelProject " : "@shopping-list/api-model" ,
これで、プロジェクトをビルドして、これまでの移行が機能していることを確認できます:
ショッピングリストアプリケーションで使用されている CloudscapeReactTsWebsiteProject は、CloudScape と Cognito 認証が組み込まれた React ウェブサイトを構成していました。
このプロジェクトタイプは create-react-app を活用していましたが、これは現在非推奨となっています。このガイドでウェブサイトを移行するために、より現代的でサポートされている技術、すなわち Vite を使用する ts#website ジェネレーター を使用します。
移行の一環として、PDK で構成された React Router から TanStack Router に移行します。これにより、ウェブサイトのルーティングに追加の型安全性が加わります。
ts#website ジェネレーター を framework を react に設定して実行し、packages/website にウェブサイトプロジェクトをセットアップします。ショッピングリストアプリケーションは CloudScape コンポーネントで構築されているため、ux を cloudscape に設定します(デフォルトは shadcn です):
このジェネレーターを実行 @aws/nx-plugin:ts#website
$ pnpm nx g @aws/nx-plugin:ts#website --name= website --framework= react --ux= cloudscape --no-interactive $ yarn nx g @aws/nx-plugin:ts#website --name= website --framework= react --ux= cloudscape --no-interactive $ npx nx g @aws/nx-plugin:ts#website --name= website --framework= react --ux= cloudscape --no-interactive $ bunx nx g @aws/nx-plugin:ts#website --name= website --framework= react --ux= cloudscape --no-interactive インストール Nx Console VSCode Plugin まだインストールしていない場合 VSCodeでNxコンソールを開く クリック Generate (UI) "Common Nx Commands"セクションで 検索 @aws/nx-plugin - ts#website 必須パラメータを入力name: websiteframework: reactux: cloudscape クリック Generate
上記の React ウェブサイトジェネレーターは、CloudscapeReactTsWebsiteProject のようにデフォルトで cognito 認証をバンドルしていません。代わりに、ts#website#auth ジェネレーター を介して明示的に追加されます。
このジェネレーターを実行 @aws/nx-plugin:ts#website#auth
$ pnpm nx g @aws/nx-plugin:ts#website#auth --project= website --cognitoDomain= shopping-list --no-interactive $ yarn nx g @aws/nx-plugin:ts#website#auth --project= website --cognitoDomain= shopping-list --no-interactive $ npx nx g @aws/nx-plugin:ts#website#auth --project= website --cognitoDomain= shopping-list --no-interactive $ bunx nx g @aws/nx-plugin:ts#website#auth --project= website --cognitoDomain= shopping-list --no-interactive インストール Nx Console VSCode Plugin まだインストールしていない場合 VSCodeでNxコンソールを開く クリック Generate (UI) "Common Nx Commands"セクションで 検索 @aws/nx-plugin - ts#website#auth 必須パラメータを入力project: websitecognitoDomain: shopping-list クリック Generate
これにより、Cognito ホストUI を使用してユーザーがログインするための適切なリダイレクトを管理する React コンポーネントが追加されます。また、packages/common/constructs に Cognito リソースをデプロイするための CDK コンストラクト(UserIdentity と呼ばれる)も追加されます。
Note
PDK の CloudscapeReactTsWebsiteProject は、アプリ内で Cognito ログインコンポーネントを提供するために @aws-northstar/ui に依存していました。ホストUI への移行ではなく、このアプローチを使用したい場合は、生成された packages/common/constructs/src/core/user-identity.ts を PDK の実装 に置き換え、@aws-cdk/aws-cognito-identitypool-alpha のコンストラクトを aws-cdk-lib が提供するものに置き換え、RuntimeConfig に関連する値を設定してください:
RuntimeConfig . ensure ( this ). set ( 'connection' , 'cognitoProps' , {
region : Stack . of ( this ). region ,
identityPoolId : this . identityPool . identityPoolId ,
userPoolId : this . userPool . userPoolId ,
userPoolWebClientId : this . userPoolClient . userPoolClientId ,
その後、CloudscapeReactTsWebsiteProject の Auth コンポーネントを再利用できます。
PDK では、生成された Projen プロジェクトを相互に渡して統合コードを生成することができました。これは、ショッピングリストアプリケーションで、ウェブサイトが API と統合できるように構成するために使用されていました。
Nx Plugin for AWS では、API 統合は connection ジェネレーター を介してサポートされています。次に、このジェネレーターを使用して、ウェブサイトが Smithy API を呼び出せるようにします:
このジェネレーターを実行 @aws/nx-plugin:connection
$ pnpm nx g @aws/nx-plugin:connection --sourceProject= website --targetProject= api --no-interactive $ yarn nx g @aws/nx-plugin:connection --sourceProject= website --targetProject= api --no-interactive $ npx nx g @aws/nx-plugin:connection --sourceProject= website --targetProject= api --no-interactive $ bunx nx g @aws/nx-plugin:connection --sourceProject= website --targetProject= api --no-interactive インストール Nx Console VSCode Plugin まだインストールしていない場合 VSCodeでNxコンソールを開く クリック Generate (UI) "Common Nx Commands"セクションで 検索 @aws/nx-plugin - connection 必須パラメータを入力sourceProject: websitetargetProject: api クリック Generate
これにより、生成された TypeScript クライアントを介してウェブサイトが API を呼び出すために必要なクライアントプロバイダーとビルドターゲットが生成されます。
CloudscapeReactTsWebsiteProject は、ショッピングリストアプリケーションで使用されている @aws-northstar/ui への依存関係を自動的に含んでいたため、@shopping-list/website プロジェクトに追加します:
pnpm add @aws-northstar/ui --filter website
yarn workspace @shopping-list/website add @aws-northstar/ui
npm install --legacy-peer-deps @aws-northstar/ui -w packages/website
bun add @aws-northstar/ui --cwd packages/website
@aws-northstar/ui は、ace-builds に依存するコードエディターコンポーネントをバンドルしており、Vite が解決できない webpack 固有のインポートを使用しています。ショッピングリストアプリケーションではこのコンポーネントを使用していないため、packages/website/vite.config.mts の既存の build オプション内の external 設定に追加してバンドルから除外します:
outDir : '../../dist/packages/website/bundle' ,
reportCompressedSize : true ,
transformMixedEsModules : true ,
external : [ 'ace-builds/webpack-resolver' ],
ショッピングリストアプリケーション には、CreateItem というコンポーネントが1つと、ShoppingList と ShoppingLists という2つのページがあります。これらを新しいウェブサイトに移行し、TanStack Router と Nx Plugin for AWS TypeScript クライアントコードジェネレーターを使用しているため、いくつかの調整を行います。
PDK プロジェクトから packages/website/src/components/CreateItem/index.tsx を新しいプロジェクトの全く同じ場所にコピーします。
packages/website/src/pages/ShoppingLists/index.tsx を packages/website/src/routes/index.tsx にコピーします。ShoppingLists はホームページであり、TanStack router でファイルベースのルーティングを使用しているためです。
packages/website/src/pages/ShoppingList/index.tsx を packages/website/src/routes/$shoppingListId.tsx にコピーします。ShoppingList は /:shoppingListId ルートで表示したいページだったためです。
IDE でいくつかのビルドエラーが表示されるようになりますが、新しいフレームワークに適合させるためにさらにいくつかの変更を行う必要があります。以下で概説します。
ファイルベースルーティング を使用しているため、ウェブサイトのローカル開発サーバーを使用してルート設定を自動的に生成できます。
ローカルウェブサイトサーバーを起動しましょう:
いくつかのエラーが表示されますが、ローカルウェブサイトサーバーはポート 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' )({
ファイルを保存すると、createFileRoute の呼び出しに関する型エラーがなくなったことに気付くでしょう。
useNavigate フックを置き換えます。
インポートを更新します:
import { useNavigate } from 'react-router-dom' ;
import { useNavigate } from '@tanstack/react-router' ;
navigate メソッド(useNavigate によって返される)の呼び出しを更新して、型安全なルートを渡すようにします:
navigate ( `/ ${ cell . shoppingListId } ` );
params : { shoppingListId : cell . shoppingListId },
useParams フックを置き換えます。
インポートを削除します:
import { useParams } from 'react-router-dom' ;
useParams の呼び出しを、上記で作成した Route によって提供されるフックで更新します。これらは型安全になりました!
const { shoppingListId } = useParams ();
const { shoppingListId } = Route . useParams ();
ルートファイルが PDK プロジェクトほどファイルツリーの深い位置にないため、routes/index.tsx と routes/$shoppingListId.tsx の両方で CreateItem のインポートを修正する必要があります:
import CreateItem from "../../components/CreateItem" ;
import CreateItem from "../components/CreateItem" ;
AppLayoutContext も新しいプロジェクトでは少し異なる場所で提供されています:
import { AppLayoutContext } from "../../layouts/App" ;
import { AppLayoutContext } from "../components/AppLayout" ;
もうすぐです!次に、Nx Plugin for AWS によって提供される TypeScript クライアントを使用するように移行する必要があります。これは Type Safe API と比較していくつかの改善があります。これを実現するには、以下の手順に従ってください
古いものの代わりに新しい生成されたクライアントと型をインポートします。例えば:
} 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" ;
routes/$shoppingListId.tsx は ShoppingList 型を _ShoppingList としてインポートしていることに注意してください - そのファイルでも同じようにしますが、再び types.gen からインポートします。
また、生成されたクライアントはフックラッパーではなく、TanStack query フックのオプションを生成するメソッドを提供するため、関連するフックを @tanstack/react-query から直接インポートすることにも注意してください。
新しい TanStack Query フックをインスタンス化します。例えば:
const getShoppingLists = useGetShoppingLists ({ pageSize : PAGE_SIZE });
const putShoppingList = usePutShoppingList ();
const deleteShoppingList = useDeleteShoppingList ();
const getShoppingLists = useInfiniteQuery (
api . getShoppingLists . infiniteQueryOptions (
{ getNextPageParam : ( p ) => p . nextToken },
const putShoppingList = useMutation ( api . putShoppingList . mutationOptions ());
const deleteShoppingList = useMutation (
api . deleteShoppingList . mutationOptions (),
リクエストボディでパラメータを受け入れる操作の呼び出しで、ラッパー <operation>RequestContent を削除します:
await putShoppingList . mutateAsync ({
putShoppingListRequestContent : {
TanStack Query v4(PDK で使用)と、connection ジェネレーターが追加した v5 の違いにより、修正すべきエラーがいくつか残っています:
ミューテーションの isLoading を isPending に置き換えます。例えば:
putShoppingList . isLoading
putShoppingList . isPending
ショッピングリストアプリケーションは、TanStack Query v4 の型を期待する @aws-northstar/ui の InfiniteQueryTable を使用していました。これは実際には v5 の無限クエリでも動作するため、型エラーを抑制するだけで済みます:
query = { getShoppingLists as any }
http://localhost:4200/ でローカルウェブサイトにアクセスできるようになりました
すべてが移行されたので、ウェブサイトが読み込まれるはずです!ショッピングリストアプリケーションが API、Website、Identity 以外に依存しているインフラストラクチャは DynamoDB テーブルだけなので、リージョン内に shopping_list という名前の DynamoDB テーブルがあり、それにアクセスできるローカル AWS 認証情報があれば、ウェブサイトは完全に機能します!
そうでない場合でも大丈夫です。次にインフラストラクチャを移行します。
チュートリアルの2つのショッピングリストページの完全な変更前後の例については、ここをクリックしてください
ショッピングリストページ /* eslint-disable @typescript-eslint/no-floating-promises */
import { InfiniteQueryTable } from "@aws-northstar/ui/components" ;
} from "@cloudscape-design/components" ;
} from "myapi-typescript-react-query-hooks" ;
import { useContext , useEffect , useMemo , useState } from "react" ;
import { useNavigate } from "react-router-dom" ;
import CreateItem from "../../components/CreateItem" ;
import { AppLayoutContext } from "../../layouts/App" ;
* Component to render the ShoppingLists "/" route.
const ShoppingLists : React . FC = () => {
const [ visibleModal , setVisibleModal ] = useState ( false );
const [ selectedShoppingList , setSelectedShoppingList ] = useState <
const getShoppingLists = useGetShoppingLists ({ pageSize : PAGE_SIZE });
const putShoppingList = usePutShoppingList ();
const deleteShoppingList = useDeleteShoppingList ();
const navigate = useNavigate ();
const { setAppLayoutProps } = useContext ( AppLayoutContext );
const columnDefinitions = useMemo <
TableProps . ColumnDefinition < ShoppingList >[]
header : "Shopping List Id" ,
href = { `/ ${ cell . shoppingListId } ` }
navigate ( `/ ${ cell . shoppingListId } ` );
cell : ( cell ) => cell . name ,
header : "Shopping Items" ,
cell : ( cell ) => ` ${ cell . shoppingItems ?. length || 0 } Items.` ,
title = "Create Shopping List"
callback = { async ( item ) => {
await putShoppingList . mutateAsync ({
putShoppingListRequestContent : {
getShoppingLists . refetch ();
isLoading = { putShoppingList . isLoading }
visibleModal = { visibleModal }
setVisibleModal = { setVisibleModal }
selectedItems = { selectedShoppingList }
onSelectionChange = { ( e ) =>
setSelectedShoppingList ( e . detail . selectedItems )
variant = "awsui-h1-sticky"
< SpaceBetween size = "xs" direction = "horizontal" >
loading = { deleteShoppingList . isLoading }
data-testid = "header-btn-delete"
disabled = { selectedShoppingList . length === 0 }
await deleteShoppingList . mutateAsync ({
shoppingListId : selectedShoppingList ![ 0 ]. shoppingListId ,
setSelectedShoppingList ([]);
getShoppingLists . refetch ();
data-testid = "header-btn-create"
onClick = { () => setVisibleModal ( true ) }
columnDefinitions = { columnDefinitions }
export default ShoppingLists ;
/* eslint-disable @typescript-eslint/no-floating-promises */
import { InfiniteQueryTable } from "@aws-northstar/ui/components" ;
} from "@cloudscape-design/components" ;
import { useContext , useEffect , useMemo , useState } from "react" ;
import { useNavigate } from "@tanstack/react-router" ;
import CreateItem from "../components/CreateItem" ;
import { AppLayoutContext } from "../components/AppLayout" ;
import { createFileRoute } from "@tanstack/react-router" ;
import { ShoppingList } from "../generated/my-api/types.gen" ;
import { useMyApi } from "../hooks/useMyApi" ;
import { useInfiniteQuery , useMutation } from "@tanstack/react-query" ;
* Component to render the ShoppingLists "/" route.
const ShoppingLists : React . FC = () => {
const [ visibleModal , setVisibleModal ] = useState ( false );
const [ selectedShoppingList , setSelectedShoppingList ] = useState <
const getShoppingLists = useInfiniteQuery (
api . getShoppingLists . infiniteQueryOptions (
{ getNextPageParam : ( res ) => res . nextToken },
const putShoppingList = useMutation ( api . putShoppingList . mutationOptions ());
const deleteShoppingList = useMutation (
api . deleteShoppingList . mutationOptions (),
const navigate = useNavigate ();
const { setAppLayoutProps } = useContext ( AppLayoutContext );
const columnDefinitions = useMemo <
TableProps . ColumnDefinition < ShoppingList >[]
header : "Shopping List Id" ,
href = { `/ ${ cell . shoppingListId } ` }
params : { shoppingListId : cell . shoppingListId },
cell : ( cell ) => cell . name ,
header : "Shopping Items" ,
cell : ( cell ) => ` ${ cell . shoppingItems ?. length || 0 } Items.` ,
title = "Create Shopping List"
callback = { async ( item ) => {
await putShoppingList . mutateAsync ({
getShoppingLists . refetch ();
isLoading = { putShoppingList . isPending }
visibleModal = { visibleModal }
setVisibleModal = { setVisibleModal }
query = { getShoppingLists as any }
selectedItems = { selectedShoppingList }
onSelectionChange = { ( e ) =>
setSelectedShoppingList ( e . detail . selectedItems )
variant = "awsui-h1-sticky"
< SpaceBetween size = "xs" direction = "horizontal" >
loading = { deleteShoppingList . isPending }
data-testid = "header-btn-delete"
disabled = { selectedShoppingList . length === 0 }
await deleteShoppingList . mutateAsync ({
shoppingListId : selectedShoppingList ![ 0 ]. shoppingListId ,
setSelectedShoppingList ([]);
getShoppingLists . refetch ();
data-testid = "header-btn-create"
onClick = { () => setVisibleModal ( true ) }
columnDefinitions = { columnDefinitions }
export const Route = createFileRoute ( '/' )({
component : ShoppingLists ,
ショッピングリストページ /* eslint-disable @typescript-eslint/no-floating-promises */
} from "@cloudscape-design/board-components" ;
} from "@cloudscape-design/components" ;
ShoppingList as _ShoppingList ,
} from "myapi-typescript-react-query-hooks" ;
import { useEffect , useState } from "react" ;
import { useParams } from "react-router-dom" ;
import CreateItem from "../../components/CreateItem" ;
type ListItem = { name : string };
* Component to render a singular Shopping List "/:shoppingListId" route.
const ShoppingList : React . FC = () => {
const { shoppingListId } = useParams ();
const [ visibleModal , setVisibleModal ] = useState ( false );
const getShoppingLists = useGetShoppingLists ({ shoppingListId });
const putShoppingList = usePutShoppingList ();
const shoppingList : _ShoppingList | undefined =
getShoppingLists . data ?. pages [ 0 ]. shoppingLists [ 0 ]!;
const [ shoppingItems , setShoppingItems ] =
useState < BoardProps . Item < ListItem >[]>();
shoppingList ?. shoppingItems ?. map (( i ) => ({
definition : { minColumnSpan : 4 },
}, [ shoppingList ?. shoppingItems ]);
variant = "awsui-h1-sticky"
< SpaceBetween size = "xs" direction = "horizontal" >
data-testid = "header-btn-create"
onClick = { () => setVisibleModal ( true ) }
Shopping list: { shoppingList ?. name }
callback = { async ( item ) => {
...( shoppingItems || []),
definition : { minColumnSpan : 4 },
putShoppingListRequestContent : {
shoppingListId : shoppingList . shoppingListId ,
shoppingItems : items . map (( i ) => i . data . name ),
visibleModal = { visibleModal }
setVisibleModal = { setVisibleModal }
onItemsChange = { ( event ) => {
const items = event . detail . items as BoardProps . Item < ListItem >[];
putShoppingListRequestContent : {
shoppingListId : shoppingList . shoppingListId ,
shoppingItems : items . map (( i ) => i . data . name ),
items = { shoppingItems || [] }
renderItem = { ( item , actions ) => (
onClick = { actions . removeItem }
dragHandleAriaLabel : "Drag handle" ,
dragHandleAriaDescription :
"Use Space or Enter to activate drag, arrow keys to move, Space or Enter to submit, or Escape to discard." ,
resizeHandleAriaLabel : "Resize handle" ,
resizeHandleAriaDescription :
"Use Space or Enter to activate resize, arrow keys to move, Space or Enter to submit, or Escape to discard." ,
liveAnnouncementDndCommitted : () => "" ,
liveAnnouncementDndDiscarded : () => "" ,
liveAnnouncementDndItemInserted : () => "" ,
liveAnnouncementDndItemReordered : () => "" ,
liveAnnouncementDndItemResized : () => "" ,
liveAnnouncementDndStarted : () => "" ,
liveAnnouncementItemRemoved : () => "" ,
navigationItemAriaLabel : () => "" ,
export default ShoppingList ;
// routes/$shoppingListId.tsx
/* eslint-disable @typescript-eslint/no-floating-promises */
} from "@cloudscape-design/board-components" ;
} from "@cloudscape-design/components" ;
import { useEffect , useState } from "react" ;
import CreateItem from "../components/CreateItem" ;
import { createFileRoute } from "@tanstack/react-router" ;
import { useMyApi } from "../hooks/useMyApi" ;
import { useInfiniteQuery , useMutation } from "@tanstack/react-query" ;
import { ShoppingList as _ShoppingList } from "../generated/my-api/types.gen" ;
type ListItem = { name : string };
* Component to render a singular Shopping List "/:shoppingListId" route.
const ShoppingList : React . FC = () => {
const { shoppingListId } = Route . useParams ();
const [ visibleModal , setVisibleModal ] = useState ( false );
const getShoppingLists = useInfiniteQuery (
api . getShoppingLists . infiniteQueryOptions (
{ getNextPageParam : ( p ) => p . nextToken },
const putShoppingList = useMutation ( api . putShoppingList . mutationOptions ());
const shoppingList : _ShoppingList | undefined =
getShoppingLists . data ?. pages ?.[ 0 ]?. shoppingLists ?.[ 0 ];
const [ shoppingItems , setShoppingItems ] =
useState < BoardProps . Item < ListItem >[]>();
shoppingList ?. shoppingItems ?. map (( i ) => ({
definition : { minColumnSpan : 4 },
}, [ shoppingList ?. shoppingItems ]);
variant = "awsui-h1-sticky"
< SpaceBetween size = "xs" direction = "horizontal" >
data-testid = "header-btn-create"
onClick = { () => setVisibleModal ( true ) }
Shopping list: { shoppingList ?. name }
callback = { async ( item ) => {
...( shoppingItems || []),
definition : { minColumnSpan : 4 },
name : shoppingList ?. name ?? 'my list' ,
shoppingListId : shoppingList ?. shoppingListId ,
shoppingItems : items . map (( i ) => i . data . name ),
visibleModal = { visibleModal }
setVisibleModal = { setVisibleModal }
onItemsChange = { ( event ) => {
const items = event . detail . items as BoardProps . Item < ListItem >[];
shoppingListId : shoppingList . shoppingListId ,
shoppingItems : items . map (( i ) => i . data . name ),
items = { shoppingItems || [] }
renderItem = { ( item , actions ) => (
onClick = { actions . removeItem }
dragHandleAriaLabel : "Drag handle" ,
dragHandleAriaDescription :
"Use Space or Enter to activate drag, arrow keys to move, Space or Enter to submit, or Escape to discard." ,
resizeHandleAriaLabel : "Resize handle" ,
resizeHandleAriaDescription :
"Use Space or Enter to activate resize, arrow keys to move, Space or Enter to submit, or Escape to discard." ,
liveAnnouncementDndCommitted : () => "" ,
liveAnnouncementDndDiscarded : () => "" ,
liveAnnouncementDndItemInserted : () => "" ,
liveAnnouncementDndItemReordered : () => "" ,
liveAnnouncementDndItemResized : () => "" ,
liveAnnouncementDndStarted : () => "" ,
liveAnnouncementItemRemoved : () => "" ,
navigationItemAriaLabel : () => "" ,
export const Route = createFileRoute ( '/$shoppingListId' )({
ショッピングリストアプリケーションで移行する必要がある最後のプロジェクトは InfrastructureTsProject です。これは TypeScript CDK プロジェクトであり、Nx Plugin for AWS の同等のものは ts#infra ジェネレーター です。
Projen プロジェクトと同様に、PDK はこれらのプロジェクトが依存する CDK コンストラクトも提供していました。ショッピングリストアプリケーションをこれらの CDK コンストラクトからも移行し、Nx Plugin for AWS によって生成されるものを使用します。
ts#infra ジェネレーター を実行して、packages/infra にインフラストラクチャプロジェクトをセットアップします:
このジェネレーターを実行 @aws/nx-plugin:ts#infra
$ 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 インストール Nx Console VSCode Plugin まだインストールしていない場合 VSCodeでNxコンソールを開く クリック Generate (UI) "Common Nx Commands"セクションで 検索 @aws/nx-plugin - ts#infra 必須パラメータを入力 クリック Generate
PDK ショッピングリストアプリケーションは、CDK アプリケーションスタック内で以下のコンストラクトをインスタンス化していました:
ショッピングリストを保存する DynamoDB テーブル用の DatabaseConstruct
PDK から直接インポートされた Cognito リソース用の UserIdentity
Smithy API をデプロイするための MyApi。これは、生成された TypeScript CDK コンストラクトを型安全な統合で使用し、内部的に PDK の TypeSafeRestApi CDK コンストラクトに依存していました。
Website をデプロイするための Website。PDK の StaticWebsite CDK コンストラクトをラップしていました。
次に、これらのそれぞれを新しいプロジェクトに移行します。
PDK ショッピングリストアプリケーションから packages/infra/src/stacks/application-stack.ts を新しいプロジェクトの全く同じ場所にコピーします。TypeScript エラーが表示されますが、以下で対処します。
PDK ショッピングリストアプリケーションには、packages/src/constructs/database.ts に Database コンストラクトがありました。これを新しいプロジェクトの全く同じ場所にコピーします。
Nx Plugin for AWS はセキュリティテストに Checkov を使用しており、PDK Nag よりも少し厳格であるため、いくつかの抑制を追加する必要があります:
import { suppressRules } from '@shopping-list/common-constructs' ;
[ '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 コンストラクトは、一般的にインポートを調整することで変更なしで置き換えることができます。
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 を使用していました。
PDK ショッピングリストアプリケーションには、constructs/apis/myapi.ts にコンストラクトがあり、Smithy モデルから Type Safe API が生成した CDK コンストラクトをインスタンス化していました。
このコンストラクトに加えて、PDK プロジェクトは @handler トレイトを使用していたため、生成された Lambda 関数 CDK コンストラクトも生成されていました。
Type Safe API と同様に、Nx Plugin for AWS は Smithy モデルに基づいて統合の型安全性を提供しますが、はるかにシンプルで柔軟な方法で実現されています。ビルド時に CDK コンストラクト全体を生成する代わりに、最小限の「メタデータ」のみが生成され、packages/common/constructs/src/app/apis/api.ts がそれを汎用的な方法で使用します。コンストラクトの使用方法の詳細については、ts#smithy-api ジェネレーターガイド を参照してください。
以下の手順に従ってください:
application-stack.ts で Api コンストラクトをインスタンス化します
import { MyApi } from "../constructs/apis/myapi" ;
import { Api } from '@shopping-list/common-constructs' ;
const myapi = new MyApi ( this , "MyApi" , {
const api = new Api ( this , 'MyApi' , {
integrations : Api . defaultIntegrations ( this ). build (),
ここで Api.defaultIntegrations(this).build() を使用していることに注目してください - デフォルトの動作は API の各オペレーションに対して Lambda 関数を作成することで、これは myapi.ts で持っていたのと同じ動作です。
Lambda 関数に DynamoDB テーブルへのアクセス権限を付与します。
PDK ショッピングリストアプリケーションでは、DatabaseConsruct が MyApi に渡され、生成された各関数コンストラクトに関連する権限を追加していました。Api コンストラクトの型安全な integrations プロパティにアクセスすることで、application-stack.ts ファイル内で直接これを行います:
// Grant our lambda functions scoped access to call Dynamo
databaseConstruct . shoppingListTable . grantReadData (
api . integrations . getShoppingLists . handler ,
api . integrations . putShoppingList . handler ,
api . integrations . deleteShoppingList . handler ,
]. forEach (( f ) => databaseConstruct . shoppingListTable . grantWriteData ( f ));
認証されたユーザーに API を呼び出す権限を付与します。
PDK アプリケーションの myapi.ts 内では、認証されたユーザーにも API を呼び出すための IAM 権限が付与されていました。application-stack.ts で同等のことを行います:
api . grantInvokeAccess ( userIdentity . identityPool . authenticatedRole );
最後に、packages/common/constructs/src/app/static-websites/website.ts から Website コンストラクトを application-stack.ts に追加します。これは PDK ショッピングリストアプリケーションの packages/infra/src/constructs/websites/website.ts と同等です。
import { Website } from "../constructs/websites/website" ;
import { Website } from '@shopping-list/common-constructs' ;
new Website ( this , "Website" , {
new Website ( this , 'Website' );
ID や API を Website に渡していないことに注意してください - ランタイム設定は Nx Plugin for AWS によって提供される各コンストラクト内で管理され、UserIdentity と Api が必要な値を登録し、Website が静的ウェブサイトの /runtime-config.json へのデプロイを管理します。
コードベースの関連部分をすべて新しいプロジェクトに移行したので、プロジェクトをビルドしましょう。
完全に移行されたコードベースが完成したので、デプロイについて見ていきましょう。この時点で2つの方法があります。
最もシンプルなアプローチは、これを完全に新しいアプリケーションとして扱うことです。つまり、新しいDynamoDBテーブルとCognito User Poolで「最初からやり直す」ことになり、すべてのユーザーとそのショッピングリストが失われます。このアプローチでは、単純に以下を実行します:
shopping_listという名前のDynamoDBテーブルを削除します
新しいアプリケーションをデプロイします:
pnpm nx deploy-sandbox infra
yarn nx deploy-sandbox infra
npx nx deploy-sandbox infra
bunx nx deploy-sandbox infra
🎉 これで完了です! 🎉
実際には、顧客のダウンタイムを避けながら、既存のAWSリソースを新しいコードベースで管理できるように移行したい場合が多いでしょう。
ショッピングリストアプリケーションの場合、重要なステートフルリソースは、ユーザーのショッピングリストを含むDynamoDBテーブルと、登録されたすべてのユーザーの詳細を含むUser Poolです。大まかな計画としては、これら2つの重要なリソースを保持し、新しいスタックで管理されるように移動してから、DNSを更新して新しいウェブサイト(および顧客に公開されている場合はAPI)を指すようにします。
保持したい既存のリソースを参照するように新しいアプリケーションを更新します。
ショッピングリストアプリケーションでは、DynamoDBテーブルに対してこれを行います
this . shoppingListTable = new Table ( this , 'ShoppingList' , {
this . shoppingListTable = Table . fromTableName (
そしてCognito User Poolに対して
this . userPool = this . createUserPool ( mfa , mfaSecondFactor );
this . userPool = UserPool . fromUserPoolId (
新しいアプリケーションをビルドしてデプロイします:
pnpm nx deploy-sandbox infra
yarn nx deploy-sandbox infra
npx nx deploy-sandbox infra
bunx nx deploy-sandbox infra
これで、既存のリソースを参照する新しいアプリケーションが立ち上がりましたが、まだトラフィックは受け取っていません。
新しいアプリケーションが期待通りに動作することを確認するために、完全な統合テストを実行します。ショッピングリストアプリケーションの場合、ウェブサイトをロードして、サインインしてショッピングリストの作成、表示、編集、削除ができることを確認します。
新しいアプリケーションで既存のリソースを参照する変更を元に戻しますが、まだデプロイはしません。
this . shoppingListTable = new Table ( this , 'ShoppingList' , {
this . shoppingListTable = Table . fromTableName (
そしてCognito User Poolに対して
this . userPool = this . createUserPool ( mfa , mfaSecondFactor );
this . userPool = UserPool . fromUserPoolId (
そしてビルドを実行します
新しいアプリケーションのpackages/infraフォルダでcdk importを使用して、インポートするように促されるリソースを確認します。
pnpm exec cdk import shopping-list-infra-sandbox/Application --force
Enterキーを押してプロンプトを進めます。リソースが別のスタックによって管理されているため、インポートは失敗します - これは予想通りです。このステップは、保持する必要があるリソースを確認するためだけに行いました。次のような出力が表示されます:
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つあることがわかります。
前のステップで発見されたリソースに対してRemovalPolicyをRETAINに設定するように、古いPDKプロジェクトを更新します。この記事を書いている時点では、User PoolとDynamoDBテーブルの両方でこれがデフォルトですが、上記で発見したSMS Roleについては更新する必要があります:
const userIdentity = new UserIdentity ( this , ` ${ id } UserIdentity` , {
const smsRole = userIdentity . userPool . node . findAll (). filter (
c => CfnResource . isCfnResource ( c ) &&
c . node . path . includes ( '/smsRole/' ))[ 0 ] as CfnResource ;
smsRole . applyRemovalPolicy ( RemovalPolicy . RETAIN );
削除ポリシーが適用されるようにPDKプロジェクトをデプロイします
CloudFormationコンソールを確認し、上記のcdk importステップで促された値を記録します
User Pool ID、例:us-west-2_XXXXX
SMS Role Name、例:infra-sandbox-UserIdentityUserPoolsmsRoleXXXXXX
リソースを作成する代わりに既存のリソースを参照するようにPDKプロジェクトを更新します
this . shoppingListTable = new Table ( this , 'ShoppingList' , {
this . shoppingListTable = Table . fromTableName (
そしてCognito User Poolに対して
const userPool = UserPool . fromUserPoolId (
const userIdentity = new UserIdentity ( this , ` ${ id } UserIdentity` , {
// PDK construct accepts UserPool not IUserPool, but this still works!
userPool : userPool as any ,
PDKプロジェクトを再度デプロイします。これにより、リソースはPDKプロジェクトのCloudFormationスタックによって管理されなくなります。
リソースが管理されていない状態になったので、新しいアプリケーションでcdk importを実行して実際にインポートを実行できます:
pnpm exec cdk import shopping-list-infra-sandbox/Application --force
プロンプトが表示されたら値を入力すると、インポートが正常に完了するはずです。
新しいアプリケーションを再度デプロイして、これらの既存のリソース(現在は新しいスタックによって管理されています)への変更が確実に行われるようにします:
pnpm nx deploy-sandbox infra
yarn nx deploy-sandbox infra
npx nx deploy-sandbox infra
bunx nx deploy-sandbox infra
新しいアプリケーションの完全なテストを再度実行します
DNSレコードを更新して、新しいウェブサイト(および必要に応じてAPI)を指すようにします。
Route53の重み付けルーティング を使用した段階的なアプローチを推奨します。これにより、最初はリクエストの一部が新しいアプリケーションに送られます。メトリクスを監視しながら、古いPDKアプリケーションにトラフィックが送られなくなるまで、新しいアプリケーションの重みを増やすことができます。
DNSがなく、ウェブサイトとAPIに自動生成されたドメインを使用している場合は、リクエストをプロキシすることを検討できます(例:CloudFront HTTPオリジン またはAPI Gateway HTTP統合 経由)。
PDKアプリケーションのメトリクスを監視してトラフィックがないことを確認し、最後に古いCloudFormationスタックを破棄します:
これはかなり複雑でしたが、ユーザーをシームレスに新しいアプリケーションに移行することに成功しました! 🎉🎉🎉
これで、PDKに対するNx Plugin for AWSの新しい利点が得られました:
より高速なビルド
ローカルAPI開発サポート
バイブコーディングに適したコードベース(MCPサーバーをお試しください! )
より直感的な型安全なクライアント/サーバーコード
その他多数!
このセクションでは、上記の移行例でカバーされていないPDKの機能に関するガイダンスを提供します。
PDKから移行する際の一般的なルールとして、PDK Monorepoとの類似性を考慮して、Nx Workspaceでプロジェクトを開始することをお勧めします。また、新しいタイプを構築する際の基本要素として、当社のジェネレーターを使用することをお勧めします。
ワークスペースを作成 @aws/nx-workspace
$ pnpm create @aws/nx-workspace my-project $ yarn create @aws/nx-workspace my-project $ npm create @aws/nx-workspace -- my-project $ bun create @aws/nx-workspace my-project
CDK Graph は、接続された CDK リソースのグラフを構築し、2つのプラグインを提供していました:
CDK Graph Diagram Plugin は、CDK インフラストラクチャから AWS アーキテクチャ図を生成します。
同様の決定論的アプローチとして、CDK-Dia が実行可能な代替手段です。
生成 AI の進歩により、多くの基盤モデルが CDK インフラストラクチャから高品質な図を作成できるようになりました。AWS Diagram MCP Server を試してみることをお勧めします。ウォークスルーについては、このブログ記事 をご覧ください。
CDK Graph Threat Composer Plugin は、CDK コードから Threat Composer の脅威モデルのスターターを生成します。
このプラグインは、脅威の例を含むベース脅威モデル を単純にフィルタリングし、スタックが使用するリソースに基づいてそれらをフィルタリングすることで機能していました。
これらの特定の脅威の例に興味がある場合は、ベース脅威モデルをコピーしてフィルタリングするか、基盤モデルが同様のものを生成するのを助けるコンテキストとして使用できます。
AWS Arch は、CloudFormation リソースとそれに関連するアーキテクチャアイコンの間のマッピングを CDK Graph 上で提供していました。
アイコン関連のリソースについては、AWS Architecture Icons ページ を参照してください。Diagrams も、コードとして図を構築する方法を提供しています。
これを直接使用していた場合は、プロジェクトをフォークして所有権を取得することを検討してください!
PDKはPDKPipelineProject を提供しており、これはCDKインフラストラクチャプロジェクトをセットアップし、いくつかのCDK Pipelines リソースをラップしたCDKコンストラクトを使用していました。
これから移行するには、CDK Pipelinesコンストラクトを直接使用できます。しかし実際には、GitHub actionsやGitLab CI/CDのようなものを使用する方がより簡単です。そこではCDK Stages を定義し、適切なステージに対してデプロイコマンドを直接実行します。
PDK Nag は CDK Nag をラップし、プロトタイプ構築に特化したルールセット を提供します。
PDK Nag から移行するには、CDK Nag を直接使用してください。同じルールセットが必要な場合は、こちらのドキュメント に従って独自の「パック」を作成できます。
Type Safe API で最もよく使用されるコンポーネントは上記の移行例でカバーされていますが、他にも機能があり、その移行の詳細は以下の通りです。
Nx Plugin for AWS は Smithy でモデル化された API をサポートしていますが、OpenAPI で直接モデル化された API はサポートしていません。ts#smithy-api ジェネレーター は良い出発点であり、その後変更することができます。model プロジェクトの src フォルダーに Smithy の代わりに OpenAPI 仕様を定義し、model プロジェクトの compile ターゲットを更新して、クライアント/サーバー用の希望するコード生成ツールを実行できます。希望するツールが NPM にある場合は、Nx ワークスペースに開発依存関係としてインストールし、Nx ビルドターゲットとして直接呼び出すことができます。
OpenAPI でモデル化された型安全なバックエンドについては、OpenAPI Generator Server Generators のいずれかの使用を検討できます。これらは AWS Lambda 用に直接生成されませんが、AWS Lambda Web Adapter を使用して、多くのギャップを埋めることができます。
TypeScript クライアントの場合、ts#website ジェネレーター と connection ジェネレーター を、サンプルの ts#api(framework を smithy に設定)と共に使用して、クライアントがどのように生成され、ウェブサイトと統合されるかを確認できます。これにより、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 の場合、上記の OpenAPI のセクションも適用されます。ts#smithy-api を生成することから始め、TypeSpec コンパイラと OpenAPI パッケージを Nx ワークスペースにインストールし、model プロジェクトの compile ターゲットを更新して代わりに tsp compile を実行し、dist ディレクトリに OpenAPI 仕様を出力するようにします。
推奨されるアプローチは、TypeSpec HTTP Server generator for JavaScript を使用してサーバーコードを生成することです。これは TypeSpec モデルで直接動作するためです。
AWS Lambda でサーバーを実行するには、AWS Lambda Web Adapter を使用できます。
上記の OpenAPI オプションのいずれかを使用することもできます。
TypeSpec には、Type Safe API がサポートする 3 つの言語すべてのクライアント用の独自のコードジェネレーターがあります:
TypeSpec は OpenAPI にコンパイルできるため、上記の OpenAPI セクションも適用されます。
上記の移行例では、ts#smithy-api ジェネレーター を使用するための移行について概説しています。このセクションでは、Python と Java のバックエンドとクライアントのオプションについて説明します。
Smithy code generator for Java 。これには Java サーバージェネレーターと、生成された Java サーバーを AWS Lambda で実行するためのアダプター があります。
Smithy には Python 用のサーバージェネレーターがないため、OpenAPI 経由で行う必要があります。潜在的なオプションについては、OpenAPI でモデル化された API に関する上記のセクションを参照してください。
Smithy code generator for Java 。これには Java クライアントジェネレーターがあります。
Python クライアントについては、Smithy Python をチェックしてください。
TypeScript については、Smithy TypeScript をチェックするか、ts#smithy-api で採用したのと同じアプローチで OpenAPI 経由で行います(tRPC、FastAPI、Smithy API 間で TanStack Query フックを介して一貫性を持たせるため、このオプションを選択しました)。
Type Safe API は、複数の Smithy ベースの API で再利用できる Smithy モデルを含むプロジェクトを構成する SmithyShapeLibraryProject という名前の Projen プロジェクトタイプを提供していました。
同等のものは、type を shapes に設定した 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-shapestype: shapes クリック Generate
SmithyShapeLibraryProject からシェイプを生成されたプロジェクトの src フォルダーに移動し、ライブラリを API のモデルの依存関係として接続する方法については、Smithy プロジェクトガイド を参照してください。
Type Safe API は、以下のデフォルトインターセプターを提供していました:
Powertools for AWS Lambda を使用したロギング、トレーシング、メトリクスインターセプター
キャッチされない例外を処理するための try-catch インターセプター
CORS ヘッダーを返すための CORS インターセプター
ts#smithy-api ジェネレーターは、Middy を使用して Powertools for AWS Lambda でロギング、トレーシング、メトリクスを計装します。try-catch インターセプターの動作は Smithy TypeScript SSDK に組み込まれており、CORS ヘッダーは handler.ts に追加されます。
任意の言語でのロギング、トレーシング、メトリクスインターセプターについては、Powertools for AWS Lambda を直接使用してください。
カスタムインターセプターの移行には、以下のライブラリの使用をお勧めします:
Type Safe API は Redocly CLI を使用したドキュメント生成を提供していました。これは、上記のように移行した既存のプロジェクトに非常に簡単に追加できます。
Redocly CLI をインストールします
pnpm add -Dw @redocly/cli
npm install --legacy-peer-deps -D @redocly/cli
redocly build-docs を使用して、model プロジェクトにドキュメント生成ターゲットを追加します。例:
" outputs " : [ "{workspaceRoot}/dist/{projectRoot}/documentation" ],
" executor " : "nx:run-commands" ,
" command " : "redocly build-docs dist/packages/api/model/build/openapi/openapi.json --output=dist/packages/api/model/documentation/index.html" ,
OpenAPI Generator documentation generators を検討することもできます。
Type Safe API は、生成されたインフラストラクチャパッケージ内でモックを生成していました。
JSON スキーマに基づいてモックデータを作成できる JSON Schema Faker に移行できます。これは OpenAPI 仕様で直接動作し、model プロジェクトビルドの一部として実行できる CLI があります。
CDK インフラストラクチャを更新して、JSON Schema Faker によって出力された JSON ファイルを読み取り、生成された metadata.gen.ts(ts#smithy-api ジェネレーター を使用したと仮定)に基づいて、統合に適切な API Gateway MockIntegration を返すことができます。
Type Safe API は、バックエンドで異なる言語の混合で API を実装することをサポートしていました。これは、CDK で API コンストラクトをインスタンス化する際に統合に「オーバーライド」を提供することでも実現できます:
const pythonLambdaHandler = new Function ( this , 'PythonImplementation' , {
runtime : Runtime . PYTHON_3_12 ,
integrations : Api . defaultIntegrations ( this )
integration : new LambdaIntegration ( pythonLambdaHandler ),
handler : pythonLambdaHandler ,
ts#smithy-api と TypeScript Server SDK を使用している場合、サービスをコンパイルするためにサービス/ルーターを「スタブ」する必要があります。例:
export const Service : ApiService < ServiceContext > = {
Echo : () => { throw new Error ( `Not Implemented` ); },
Type Safe API は、SpecRestApi コンストラクトを内部で使用していたため、OpenAPI 仕様に基づいてリクエストボディのネイティブ API Gateway 検証を追加していました。
ts#smithy-api ジェネレーター では、検証は Server SDK 自体によって実行されます。これはほとんどのサーバージェネレーターで同じです。
ネイティブ API Gateway 検証を実装したい場合は、packages/common/constructs/src/core/api/rest-api.ts を変更して、OpenAPI 仕様から各操作のリクエストボディの関連する JSON スキーマを読み取ることで実装できます。
残念ながら、API Gateway と Lambda を使用した Type Safe API の websocket API のモデル駆動型 API 開発への簡単な移行パスはありません。ただし、ガイドのこのセクションでは、少なくともいくつかのアイデアを提供することを目的としています。
OpenAPI や TypeSpec の代わりに AsyncAPI を使用して API をモデル化することを検討してください。これは非同期 API を処理するように設計されているためです。AsyncAPI NodeJS Template は、たとえば ECS でホストできる Node websocket バックエンドを生成できます。
インフラストラクチャには AppSync Events を検討し、Powertools を使用することもできます。このブログ投稿 は一読の価値があります!
別のオプションは、AppSync で websocket を使用した GraphQL API を使用することです。これについては GitHub issue があり、+1 できます!詳細とサンプルプロジェクトへのリンクについては、AppSync developer guide を参照してください。
Type Safe API と同じベンダー拡張を解釈する独自のコードジェネレーターを構築することも検討できます。カスタム OpenAPI ベースのコードジェネレーターの構築に関する詳細については、OpenAPI でモデル化された API セクションを参照してください。Type Safe API が API Gateway Websocket API Lambda ハンドラーに使用するテンプレートはこちら 、クライアントはこちら にあります。
tRPC を使用するために ts#trpc-api ジェネレーター への移行を検討することもできます。執筆時点では、サブスクリプション/ストリーミングのサポートはまだありませんが、これが必要な場合は、これを追跡している GitHub issue に +1 を追加してください。
Smithy はプロトコルに依存しませんが、Websocket プロトコルのサポートはまだありません。サポートを追跡しているこの GitHub issue を参照してください。
PDK は Python と Java で書かれた CDK インフラストラクチャをサポートしていました。現時点では、Nx Plugin for AWS ではこれをサポートしていません。
推奨される進め方は、CDK インフラストラクチャを TypeScript に移行するか、または当社のジェネレーターを使用して共通コンストラクトパッケージを希望する言語に移行することです。このような移行を加速するために、生成 AI を使用できます。例えば Kiro CLI などです。合成された CloudFormation テンプレートが同一になるまで、AI エージェントに移行を繰り返させることができます。
Type Safe API の Python または Java で生成されたインフラストラクチャについても同様です。共通コンストラクトパッケージから汎用的な rest-api.ts コンストラクトを翻訳し、ターゲット言語用の独自のシンプルなメタデータジェネレーターを実装できます(OpenAPI でモデル化された API セクションを参照してください)。
CDK コードを追加するための基本的な Python プロジェクトには、py#project ジェネレーター を使用できます(cdk.json ファイルを移動し、関連するターゲットを追加します)。Java プロジェクトには Nx の @nx/gradle プラグインを使用するか、Maven の場合は @jnxplus/nx-maven を使用できます。
PDKはProjen の上に構築されました。ProjenとNx Generatorsには根本的な違いがあり、技術的には組み合わせることは可能ですが、アンチパターンになる可能性があります。Projenはプロジェクトファイルをコードとして管理するため、直接変更することはできませんが、Nx generatorsはプロジェクトファイルを一度生成した後、コードを自由に変更できます。
Projenを引き続き使用したい場合は、必要なProjenプロジェクトタイプを自分で実装できます。Nx Plugin for AWSのパターンに従うには、当社のgeneratorsを実行するか、GitHubでそのソースコードを調べて、必要なプロジェクトタイプがどのように構築されているかを確認し、Projenのプリミティブを使用して関連部分を実装できます。