🕌

CDK Toolkit Library で対話的なコマンド実行を実現する

に公開
2025/11/07

この記事では、CDK Toolkit Library を使用して対話的なコマンド実行を実現する方法を解説します。

CDK Toolkit Library とは

CDK Toolkit Library (@aws-cdk/toolkit-lib) (以下 Toolkit Library) とは、AWS CDK CLI (以下 CDK CLI) の機能(デプロイ、差分確認、合成など)を TypeScript プログラム内から直接実行できるライブラリです。

この Toolkit Library と CDK CLI にはいくつかの違いがあり、その一つとして「コマンド実行時にユーザーの承認を必要とするかどうか」が挙げられます。

CDK CLI を使用した場合、deploy などのコマンドを実行する際に --require-approval オプションが指定でき、この値によって、「コマンド実行時にユーザー承認を必要とするか」を指定できます。
また、この --require-approval オプションのデフォルト値は BROADENING に設定されています(※1)

一方、Toolkit Library の方では RequireApproval によってユーザーの承認の有無を指定することはできず、追加の設定を行わない限り常にユーザーの承認を必要としません。
(これは RequireApproval に NEVER を指定した挙動と、ほとんど同じになります)

※1. 参照:

Toolkit Library を使用してユーザー承認を実現する

先述したような、コマンド実行時にユーザーに入力を求めたり、コンソールにメッセージを書き込むといった機能は、CDK では IO Host という仕組みによって実現されています。
具体的に、CDK CLI では CliIoHost クラス、Toolkit Library では NonInteractiveIoHost クラスでそのような仕組みを実装しています

(先ほど Toolkit Library ではデフォルトでユーザー承認が無効化されていると述べましたが、これは Toolkit Library が使用する NonInteractiveIoHost クラスがユーザー承認を必要としない実装になっているためです)

この NonInteractiveIoHost という名前の通り、Toolkit Library で使用する IO Host にこのクラスを使用する限りはユーザー承認は常に無効化されてしまう為、ユーザー承認を有効化したい場合には以下のように、Toolkit Library に指定する ioHost プロパティを変更する必要があります。

// declare class InteractiveIoHost implements IIoHost { /** ... */ }
import { Toolkit } from '@aws-cdk/toolkit-lib';

new Toolkit({
  // ...
  ioHost: new InteractiveIoHost(),
});


ただ、ここで指定する IO Host の仕組みを実装したクラス(CliIoHost クラスなど)は、aws-cdk-cli リポジトリからは提供されていません。(NonInteractiveIoHost クラスを除いて)

そのため、Toolkit Library を使用してユーザー承認を有効化したい場合は、以下のコードのように、IO Host の仕組みを実現したクラスを自前で実装する必要があります。

コード例
import * as readline from 'node:readline/promises';
import * as util from 'node:util';
import { IIoHost, IoRequest, NonInteractiveIoHost, NonInteractiveIoHostProps, PermissionChangeType } from '@aws-cdk/toolkit-lib';
import { RequireApproval } from 'aws-cdk-lib/cloud-assembly-schema';

type IoRequestMessage = IoRequest<
  { permissionChangeType: PermissionChangeType; motivation?: string; concurrency?: number; responseDescription?: string; } | undefined,
  unknown
>;

interface InteractiveIoHostProps extends NonInteractiveIoHostProps {
  /**
   * 承認が必要な変更のレベル
   * @default RequireApproval.BROADENING
   */
  readonly requireApproval?: RequireApproval;
}

export class InteractiveIoHost extends NonInteractiveIoHost implements IIoHost {
  private readonly requireDeployApproval: RequireApproval;

  constructor(props?: InteractiveIoHostProps) {
    super(props);
    this.requireDeployApproval = props?.requireApproval ?? RequireApproval.BROADENING;
  }

  // NonInteractiveIoHost の requestResponse メソッドではユーザーとの対話的なやり取りができない為、`requestResponse` メソッドを override して対話的なやり取りを実装する
  // 実装は aws-cdk-cli の CliIoHost の requestResponse メソッドを参考にしている
  // ref: https://github.com/aws/aws-cdk-cli/blob/@aws-cdk/toolkit-lib@v1.2.4/packages/aws-cdk/lib/cli/io-host/cli-io-host.ts
  override async requestResponse<DataType, ResponseType>(
    msg: IoRequest<DataType, ResponseType>
  ): Promise<ResponseType> {
    const message = msg as IoRequestMessage;
    if (this.skipApprovalStep(message)) return true as ResponseType;

    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout,
    });

    try {
      // defaultResponse が boolean でない場合は、プロンプト情報を抽出して表示し、ユーザーの入力を待つ。その後、ユーザーが回答した内容を変換して返す
      if (typeof msg.defaultResponse !== 'boolean') {
        const prompt = this.extractPromptInfo(message);
        const desc = message.data?.responseDescription ?? prompt.default;
        const answer = await rl.question(`${message.message}${desc ? ` (${desc})` : ''}: `);
        const finalAnswer = answer.trim() || prompt.default;
        return prompt.convertAnswer(finalAnswer) as ResponseType;
      }
      // defaultResponse が boolean の場合は、承認を求めるメッセージを表示し、ユーザーの入力を待つ
      const answer = await rl.question(`${msg.message} (y/n): `);
      const confirmed = answer.toLowerCase().trim() === 'y' || answer.toLowerCase().trim() === 'yes';
      if (!confirmed) throw new Error('Aborted by user');
      return confirmed as ResponseType;
    } finally {
      rl.close();
    }
  }

  /**
   * 指定されたメッセージが承認をスキップできるかどうかを判定する
   * @returns 承認をスキップできる場合は true
   */
  private skipApprovalStep(msg: IoRequestMessage): boolean {
    // `CDK_TOOLKIT_I5060` は、セキュリティ機密の変更を確認するコード
    // https://github.com/aws/aws-cdk-cli/blob/@aws-cdk/toolkit-lib@v1.2.4/packages/@aws-cdk/toolkit-lib/docs/message-registry.md
    const approvalToolkitCodes = ['CDK_TOOLKIT_I5060'];
    if (!approvalToolkitCodes.includes(msg.code)) return false;
    switch (this.requireDeployApproval) {
      case RequireApproval.NEVER:
        return true;
      case RequireApproval.ANYCHANGE:
        return false;
      case RequireApproval.BROADENING:
        return ['none', 'non-broadening'].includes(msg.data?.permissionChangeType ?? '');
    }
  }

  /**
   * リクエストからプロンプト情報を抽出する
   * @returns プロンプト情報を含むオブジェクト
   */
  private extractPromptInfo(request: IoRequest<unknown, unknown>): {
    default: string;
    defaultDesc: string;
    convertAnswer: (input: string) => string | number;
  } {
    const defaultResponse = util.format(request.defaultResponse);
    return {
      default: defaultResponse,
      defaultDesc: 'defaultDescription' in request && request.defaultDescription ? util.format(request.defaultDescription) : defaultResponse,
      convertAnswer: typeof request.defaultResponse === 'number' ? v => Number(v) : v => String(v),
    };
  }
}

今後の改善予定

以前、aws-cdk-cli リポジトリに、ユーザーとの対話を必要とする(あるいは必要の有無を指定できる) InteractiveIoHost クラスの実装を提案する issue が投稿されていましたが、こちらは not planned としてクローズされています。

https://github.com/aws/aws-cdk-cli/issues/761

その為、しばらくの間は自前で実装する必要がありそうです。

(ただ、この issue のコメントにもあるように、Toolkit Library の IO Host 周りの機能が安定してきた段階で、ユーザー承認を必要とする IO Host の実装が再度検討される可能性があります)

参考文献

https://go-to-k.hatenablog.com/entry/cdk-toolkit-cli-comparison

GitHubで編集を提案

Discussion