【AWS CDK】AWS CDK Toolkit LibraryがGAされました（その１）

## 今回のお話  
いままでプレビュー版だったAWS CDK Toolkit Libraryが、5/30（金）についにGAされました。  
  
https://aws.amazon.com/jp/about-aws/whats-new/2025/05/aws-cdk-toolkit-library-available/
  
そこで今回は、AWS CDK Toolkit Libraryについて2回に分けて紹介したいと思います。    
  
- AWS CDK Toolkit Libraryの概要とコア機能（今回）
- AWS CDK Toolkit Libraryの設定回り、エラーハンドリング（次回）  
  
## 参考サイト  
- [Perform programmatic actions using the CDK Toolkit Library](https://docs.aws.amazon.com/cdk/v2/guide/toolkit-library.html)
  - AWS CDK Toolkit Library公式ドキュメント
- [Build Custom CLI's, Deployment Automation, and more with the AWS CDK Toolkit Library](https://community.aws/content/2wm6TNpUlPMVgcvXVySywxWaO7T/build-custom-cli-s-deployment-automation-and-more-with-the-aws-cdk-toolkit-library)
  - Adam Keller氏によるAWS CDK Toolkit Libraryの紹介ブログ  
  
## AWS CDK Toolkit Libraryとは？  
今までCDK CLIで行っていたCDKの各種機能（```cdk synth```, ```cdk deploy```など）を、（CLI経由ではなく）アプリケーション同様プログラムコードを介して実行可能なライブラリ。  
  
現在はNode.jsにのみ対応しています。  
  
## アプリケーション的に実行できると何がいいの？（AWS CDK Toolkit Libraryの特徴・メリット）  
  
### インフラライフサイクルの細かいハンドリング＆CDKソースとの統合
  
- ```synth```, ```deploy``` などをアプリ的に実行することで、成功・失敗時の挙動やエラーハンドリングなどをコードで細かく制御できます。  
- それにより、進行状況を監視し、インジケータで表示したり細かいログ制御を行ったり...といったカスタマイズも可能です。  
- CDKをインフラライフサイクルのワークフローに統合し、組織やチームで専用のアプリを構築する、といったことも可能になります。 
  
### インテグレーションテスト  
  
- AWS SDKを用いたインテグレーションテスト（Lambdaなどを実際にAWSにデプロイしたあとにAWS上のLambdaを実行し、インフラとアプリロジックが正しいことを確認するテスト）を比較的容易に実行することができます。[^1]  
     
[^1]:ただし、AWS CDK Toolkit Library自体にテストおよびテストランナーの機能はありません   
  
### 一時的な環境の動作検証  
PoCやお試し用などで作成した一時的な環境の動作検証・破棄をCLIを使うことなく容易に実現可能になります。  
  
### 「コードベース」による恩恵
アプリケーション的にコードベースの記載になるため、下記のようなコードの機能による恩恵（？）を受けることができます。  
  
- TypeScriptによる型の恩恵（構成やコンパイルの問題早期など）を受けられる
- 命名規則、セキュリティ ベースライン、コーディングルールなどを一元管理化し、コードレベルで自動的に適用可能（各種Linter, cdk-nag, zodなど） 
- 詳細に構造化されたエラー処理機構を実装可能
  - AWS CDK Toolkit Libraryには独自のエラーハンドリング機能が用意されています。（次回紹介します）   
  
  
## 早速触ってみる  
では、早速AWS CDK Toolkit LibraryでAWS CDKの一通りの機能を実行してみましょう。  
  
下記サンプルコード[^2]をベースに説明します。  

[^2]:先ほど「参考サイト」で紹介したAdam Keller氏のブログの「Walkthrough: Ephemeral Environment with Automated Testing」のコードをベースに、一部修正したものです
  
```typescript
import { Toolkit, StackSelectionStrategy, AssemblyError } from '@aws-cdk/toolkit-lib';
import { App } from 'aws-cdk-lib';
import { AwsCdkToolkitLibraryDemoStack } from './lib/aws-cdk-toolkit-library-demo-stack';
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
  
const NO_ERROR = 0;
const WITH_ERROR = 1;
  
// クラウドアセンブリ作成関数
async function createCloudAssembly(toolkit: Toolkit, stackName: string) {
  console.info('Creating CloudAssebmly...');
  const assembly = await toolkit.fromAssemblyBuilder(async () => {
    const app = new App();
    new AwsCdkToolkitLibraryDemoStack(app, stackName);
    return app.synth();
  });
  console.info('Creating Successful');
  return assembly;
}
  
// デプロイ関数
async function deployStack(toolkit: Toolkit, cloudAssembly: any, stackName: string) {
  console.info(`Deploying stack: ${stackName}...`);
  const result = await toolkit.deploy(cloudAssembly, {
    stacks: {
      strategy: StackSelectionStrategy.PATTERN_MUST_MATCH,
      patterns: [stackName],
    },
  });
  console.info('Deployment successful.');
  
  const stack = result.stacks.find((s) => s.stackName === stackName);
  if (!stack) {
    throw new Error(`Stack ${stackName} not found after deployment`);
  }
  
  return stack;
}
  
// インテグレーションテスト実行関数（必要に応じて実行）
async function invokeLambda(functionArn: string, payload: any): Promise<void> {
  console.info('Invoking Lambda with payload...');
  const lambdaClient = new LambdaClient({});
  const invokeCommand = new InvokeCommand({
    FunctionName: functionArn,
    Payload: Buffer.from(JSON.stringify(payload)),
  });
  
  const response = await lambdaClient.send(invokeCommand);
  const responseData = JSON.parse(Buffer.from(response.Payload ?? []).toString());
  
  if (responseData.statusCode && responseData.statusCode === 200) {
    console.info('Invoke Lambda passed');
  } else {
    console.error('Invoke Lambda failed');
    console.error(responseData);
  }
  
  return;
}
    
// スタック削除関数（必要に応じて）
async function destroyStack(toolkit: Toolkit, cloudAssembly: any, stackName: string) {
  console.info(`Destroying stack: ${stackName}...`);
  try {
    await toolkit.destroy(cloudAssembly, {
      stacks: {
        strategy: StackSelectionStrategy.PATTERN_MUST_MATCH,
        patterns: [stackName],
      },
    });
    console.info('Stack destroyed successfully.');
  } catch (error) {
    console.error('Error during stack destruction:');
    throw error;
  }
}
   
// メイン関数
async function main() {
  // Toolkitインスタンス作成
  const toolkit = new Toolkit();
  const stackName = 'AwsCdkToolkitLibraryDemoStack';
  let cloudAssembly;
  
  // ここはコードを省略しているが、インテグレーションテストやスタック削除は
  // フラグを設定し、フラグがtrueの場合のみ実行する
  const isIntegTest = ...
  const isDestroyStack = ...
      
  try {
    // クラウドアセンブリ作成
    cloudAssembly = await createCloudAssembly(toolkit, stackName);
    
    // デプロイ
    const stack = await deployStack(toolkit, cloudAssembly, stackName);
    
    // インテグレーションテスト
    if (isIntegTest) {
      // Lambda関数のARNをCFnの「出力」から取得。  
      // ただしLambda関数のARNのフォーマットは固定なので、
      // わざわざCFnの「出力」から取得しなくてもOK
      const functionArn = stack.outputs['functionArn'];
      await invokeLambda(functionArn, { payload: 'test-failed' });
    }
    
    // スタック削除
    if (isDestroyStack) {
      await destroyStack(toolkit, cloudAssembly, stackName);
    }
  
    process.exit(NO_ERROR);
    
  } catch (error) {
    console.error('Error during this harness:');
    console.error(error);

    process.exit(WITH_ERROR);
  }
}
    
// メイン処理呼び出し
main();
```

### AWS CDK Toolkit Libraryのインストール
下記コマンドを実行するだけです。  
  
```sh
npm install @aws-cdk/toolkit-lib
```
  
AWS CDK Toolkit Library以外のAWS CDKの環境構築の手順は、今までと同じです。（一応簡単なコマンドを下に書いておきます）  
※もちろんAdam Keller氏のブログに書いてある手順でもOKです  
  
```sh
# とりあえずプロジェクトフォルダ作って
mkdir aws-cdk-toolkit-library-demo
cd aws-cdk-toolkit-library-demo
  
# npx cdk initすればそれっぽい構成ができる
# フォルダが空じゃないとエラーになる
npx cdk init --language typescript
  
# あとはnpm install すればCDKに最低限必要なものはすべてインストールされる  
npm install
  
# その他AWS SDK JapaScript v3やLambdaの型定義など、必要なものをインストールする
npm i @aws-sdk/client-lambda
npm i -D @types/aws-lambda
```
  
### StackおよびLambda関数の定義
まずはAWS CDKでStack、およびLambda関数を定義します。（ここも今までと同じです。確認したい場合は下記を参考）  
  
```typescript
// aws-cdk-toolkit-library-demo/lib/aws-cdk-toolkit-library-demo-stack.ts
// ↑のファイル名は適宜置き換えてください。
  
// Stack定義
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { LoggingFormat } from 'aws-cdk-lib/aws-lambda';
import { RetentionDays } from 'aws-cdk-lib/aws-logs';
import path from 'node:path';
  
export class AwsCdkToolkitLibraryDemoStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
  
    // 「entry」にpath.resolveで絶対パス指定しているのがポイント
    const demoFunc = new NodejsFunction(this, 'DemoFunction', {
      entry: path.resolve(__dirname, '../lambda/index.ts'),
      functionName: 'CdkToolkitDemoFunction',
      runtime: cdk.aws_lambda.Runtime.NODEJS_22_X,
      timeout: cdk.Duration.seconds(30),
      handler: 'handler',
      loggingFormat: LoggingFormat.JSON,
      logRetention: RetentionDays.ONE_WEEK,
    });
  
    demoFunc.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE);
    
    // CFnの「出力」にLambda関数のARNを出力しておくのもポイント
    new cdk.CfnOutput(this, 'functionArn', {
      value: demoFunc.functionArn,
    });
  }
}
```
    
```typescript
// aws-cdk-toolkit-library-demo/lambda/index.ts
// ↑のファイル名は適宜置き換えてください。
  
// Lambda関数定義
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
  
// 見てわかる通り、お試し用Lambdaです。
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
  return {
    statusCode: 200,
    body: 'OK',
  };
};
```
  
### AWS CDK Toolkit Libraryの挙動定義
ではここから、AWS CDK Toolkit Libraryの挙動を定義するファイルを作成します。  
「aws-cdk-toolkit-library-demo」フォルダ直下に「aws-cdk-toolkit-library.ts」というファイルを作成してください。  
  
なおAWS CDK Toolkit Libraryでの（最低限必要な）処理の流れは、以下の通りになります。  
  
1. Toolkitインスタンス作成
1. クラウドアセンブリ作成
1. デプロイ  
  
  
#### 1. Toolkitインスタンス作成  
```new Toolkit()```するだけです。  
  
またこの時、デプロイ時などに使用するAWSプロファイル情報を指定することもできます。（下記ソース参照）
   
```typescript
import { Toolkit } from '@aws-cdk/toolkit-lib';
  
// 普通にインスタンス作成する場合はこれでOK
const toolkit = new Toolkit();  
  
// 下記のように指定することで、AWSプロファイル情報を指定可能
const toolkit = new Toolkit({
  sdkConfig: { profile: "xxxxx" },
});
  
```
  
#### 2. クラウドアセンブリ作成  
「クラウドアセンブリ」はCDKソースからCloudFormationテンプレートを作成ためのもので[^3]、Toolkitクラスの以下3つのメソッドから作成できます。  

[^3]:正確には、そのための手段を提供するもの
  
|メソッド|説明|具体的なソース|
|:--|:--|:--|
|fromAssemblyBuilder|引数に指定したインラインソースを使用して作成する|インラインソースで直にスタック作成しても、importしたスタックを使用しても良い|
|fromCdkApp|既存のCDKアプリケーション(app)から作成する|例：app.tsなど|
|fromAssemblyDirectory|すでにcdk synthなどで作成されたクラウドアセンブリがあるディレクトリを参照して作成する|例：「cdk.out」など|  
  
```typescript
// 注：toolkitインスタンスは作成済の前提
// fromAssemblyBuilderの例
import { App } from 'aws-cdk-lib';
import { AwsCdkToolkitLibraryDemoStack } from './lib/aws-cdk-toolkit-library-demo-stack';
    
const assembly = await toolkit.fromAssemblyBuilder(async () => {
  const app = new App();  
  // スタックはCDKからimportしても、ここでclass定義をインラインで書いても良い
  new AwsCdkToolkitLibraryDemoStack(app, 'AwsCdkToolkitLibraryDemoStack');
  return app.synth();
});
  
// fromCdkAppの例    
const assembly = await toolkit.fromCdkApp("ts-node app.ts");
  
// fromAssemblyDirectoryの例    
const assembly = await toolkit.fromAssemblyDirectory("cdk.out");
```
  
#### 3. デプロイ
Toolkitクラスの ```deploy(assembly)``` メソッドを実施すればOKです。  
※第1引数のクラウドアセンブリは必須です。（これはほぼすべてのToolkitクラスのメソッドに当てはまります）  
  
また第2引数で、デプロイするスタックの指定やパラメータ・デプロイメソッドなどを指定可能です（この辺の詳細は次回触れます）
  
```typescript
import { StackSelectionStrategy } from '@aws-cdk/toolkit-lib';

// toolkit.deployでデプロイを実行する。
// 第2引数に下記の指定することで、デプロイするスタックを指定できる。
// （未指定の場合、全スタックがデプロイされる）
const stackName = 'xxxx';
  
const result = await toolkit.deploy(assembly , {
  stacks: {
    strategy: StackSelectionStrategy.PATTERN_MUST_MATCH,
    patterns: [stackName],
  },
});
  
// deploy()メソッドの戻り値から、デプロイしたスタックを取得可能。
// 後でインテグレーションテストなどを実施する場合に使用する
const stack = result.stacks.find((s) => s.stackName === stackName);
if (!stack) {
  throw new Error(`Stack ${stackName} not found after deployment`);
}
  
return stack;
```

### その他の処理の実行  
その他の処理として、下記の3つを説明します  
  
- Lambda実行（インテグレーションテスト）
- 手動ロールバック
- スタック削除  
  
#### Lambda実行（インテグレーションテスト）  
AWS CDK Toolkit Library自体にはインテグレーションテストの機能はありませんが、アプリケーション統合したことでインテグレーションテストがやりやすくなっています。  
  
インテグレーションテスト自体は、ソース内部でAWS SDKを使用して実施します。  
  
```typescript
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';  
  
const payload = {
  payload: 'test-failed',
};
  
const lambdaClient = new LambdaClient({});
const invokeCommand = new InvokeCommand({
  // stackは「デプロイ」処理で取得した「stack」定数
  FunctionName: stack.outputs['functionArn'];,
  Payload: Buffer.from(JSON.stringify(payload)),
});
  
const response = await lambdaClient.send(invokeCommand);
const responseData = JSON.parse(Buffer.from(response.Payload ?? []).toString());
  
if (responseData.statusCode && responseData.statusCode === 200) {
  console.info('Invoke Lambda passed');
} else {
  console.error('Invoke Lambda failed');
  console.error(responseData);
}
    
return;
```
  
#### 手動ロールバック＆スタック削除
手動ロールバック、およびスタック削除はそれぞれToolkitクラスの ```rollback(assembly)``` および ```destroy(assembly)``` メソッドで実施できます。  
  
この2つは使い方が似ているので、まとめて下記ソースを参照してください。  
  
なお、これらの注意点は下記の通りです。  
  
- 基本的にロールバックはデプロイが失敗した際、自動で実施されます。この自動ロールバックが失敗して手動でロールバックしなければならない際に```toolkit.rollback()```を実施してください。  
- CDK CLIと違い、```toolkit.destroy()``` を実行すると <span style="color: #ff0000">**<u>確認メッセージを表示することなくダイレクトにスタックを削除します</u>**</span>。実行には十分注意してください。
  
```typescript
import { StackSelectionStrategy } from '@aws-cdk/toolkit-lib';
  
const stackName = 'xxxx';
// 下記の通り、どちらも第二引数でdeploy()と同じオブジェクトを
// 指定することで、対象のスタックを指定可能
  
// 手動ロールバック
await toolkit.rollback(assembly , {
  stacks: {
    strategy: StackSelectionStrategy.PATTERN_MUST_MATCH,
    patterns: [stackName],
  },
});
  
// スタック削除
await toolkit.destroy(assembly , {
  stacks: {
    strategy: StackSelectionStrategy.PATTERN_MUST_MATCH,
    patterns: [stackName],
  },
});
```  
  
### その他  
今回は触れませんでしたが、CDK CLIにあった下記コマンドもAWS CDK Toolkit Libraryのメソッドとして用意されていますので、気になる方は一度実行してみてください。  
  
- bootstrap
- list 
- diff
- drift
- watch  など
  

## まとめ  
以上、AWS CDK Toolkit Libraryの概要でした。  
  
まだGAされたばかりで、よく内容がわからないかもしれませんが、ちょっと使用した限り、今のところ大きな不便もなく問題なく使えています。  
  
またアプリ統合されたことでいろいろ細かい制御やコードベースならではの恩恵を受けられる点は便利なので、今後もしばらくはAWS CDK Toolkit Libraryを使っていこうと思っています。
  
     
  
## 宣伝
昨年開催された「AWS CDK Conference Japan」が、今年も開催されます。
  
https://jawsug-cdk.connpass.com/event/356357/
  
昨年同様、今年も有識者の発表に加え、下記のようなワークショップが開催されますので、気になった方はぜひ参加しましょう。  
  
- 【初心者ワークショップ】AWS CDK Conference Japan 2025
- 【CDKコントリビュートワークショップ】AWS CDK Conference Japan 2025
- 【Amplifyワークショップ】AWS CDK Conference Japan 2025  
  
また、私も現在CFPを1つ提出しておりますので、よろしければよろしくお願いします。  
  
https://fortee.jp/jawsug-cdk-conference-2025/proposal/8a8d67b2-4c3c-44b7-aacb-dc86e0363912
   
なお、AWS CDK Conference Japan 2025の1週間前の7/5（土）には、愛知県豊橋市で「JAWSミート2025」が今年も開催されます。  
  
https://jaws-ug-tokaido.connpass.com/event/355144/
  
こちらもぜひ、皆さん参加しましょう！
  
  
それでは、今回はこの辺で。