AWS CDK にはアプリケーションライフサイクルというものがあり、この一連の流れを知ることで、CDK アプリケーションの動作をより深く理解することができます。
目次
はじめに
AWS CDK は、いくつかのプログラミング言語で記述できる IaC ツールです。つまり、CDK はデプロイのためのツールです。
そのデプロイ処理の中には、「合成(Synthesize)」という CDK の特徴的な処理があります。この合成処理では、ユーザが実装した CDK アプリケーションのコードを実行し、それをもとに変換された CloudFormation テンプレートを含む、クラウドアセンブリ(Cloud Assembly) と呼ばれる中間成果物を cdk.out ディレクトリに生成します。
そして、合成によって生成されたクラウドアセンブリをもとに、CDK CLI が CloudFormation を通して AWS リソースのデプロイを行います。
アプリケーションライフサイクル
先ほど説明した合成処理の中で、CDK アプリケーションはいくつかのフェーズに分かれて実行されます。この一連の流れは「アプリケーションライフサイクル」と呼ばれています。
そのライフサイクルは、以下の 4 つのフェーズに分類されます。
- Construct フェーズ (Construction Phase)
- Prepare フェーズ (Preparation Phase)
- Validate フェーズ (Validation Phase)
- Synthesize フェーズ (Synthesis Phase)

※以下の図は CDK の公式ドキュメントの AWS CDK アプリケーションのデプロイ に掲載されているものですが、より CDK の全体の流れがイメージできるかと思います。

Construct フェーズ

Construct ツリーの構築
このフェーズでは、ユーザの定義した CDK コードを上から順に実行していき、宣言された Construct のインスタンスが生成されます。
まず App を呼び出し、その中で Stage もしくは Stack が生成されます。そして Stack の中で、CDK で提供される Construct や、ユーザが定義した Construct を呼び出していきます。Construct は別の Construct の中でも呼び出せるため、それらはネストした構造になります。
それらの流れを通して各インスタンスはツリー状に構築されていき、いわゆるConstruct ツリーが形成されます。

また基本的には、ユーザの書いた CDK コードのほとんどがこの Construct フェーズで実行されます。
そのため以降のフェーズでは、構築された Construct ツリーに対して何らかの操作を行うことが主な役割となります。
Prepare フェーズ

Aspects
ツリーの完成後、ここではまず、ツリーのルートから順に Aspect が適用されているか確認します。そして、適用されていた場合は、適用された Construct のノードから子や孫のノード全てに対して、その Aspect で定義された処理が実行されます。
例えば、先ほど挙げた Construct ツリーの例の中で、MyConstruct に MyAspect がアタッチされていた場合、MyAspect の処理は MyConstruct のノードから子や孫のノード全てに対して実行されることになります。
const myConstruct = new MyConstruct(this, 'MyConstruct'); Aspects.of(myConstruct).add(new MyAspect());

Aspect はスタックや Construct にアタッチした時点では実行されず、すべての Stack や Construct のコードが実行された後に実行されることになります。この仕組みを理解していないと、想定するタイミングで Aspect が実行されていないことに混乱することがあるため、注意が必要です。
const myConstruct = new MyConstruct(this, 'MyConstruct'); Aspects.of(myConstruct).add(new MyAspect()); // MyConstruct のツリーにリソースを後から追加する myConstruct.addResource(); // MyAspect の処理はこの処理の前に実行されるわけではない
その他の Prepare フェーズの処理
Prepare フェーズでは、Aspects の適用以外にも以下のような処理が行われます。
- リソース間の依存 (
DependsOn) の追加- Construct (L2 Construct) 間に追加された依存をリソース (L1 Construct) 間に落とし込む
- スタック間の依存関係の解決
- クロススタック参照:
Outputs+ImportValueを使用 - クロスリージョン参照: カスタムリソースを使用
- ネストスタック参照:
OutputsやParametersを使用
- クロススタック参照:
- ネストスタック処理
- 子スタックのテンプレートを先に作成して、親スタックの S3 アセットとして追加
Validate フェーズ

バリデーションとは
このフェーズでは、各 Construct にアタッチされたバリデーションが発火されます。
ここでいうバリデーションとは、IValidation インターフェースを実装したオブジェクトで、validate メソッド内にバリデーション処理を記述します。validate メソッドは、エラーメッセージの配列を返すようになっているため、複数のエラーをまとめて返すことが可能です。
export class Validator implements IValidation { constructor(private readonly someParam: string) {} validate(): string[] { const errors = []; if (this.someParam === 'invalid name') { errors.push('someParam must not be "invalid name"'); } if (this.someParam.includes('invalid')) { errors.push('someParam must not include "invalid"'); } return errors; } }
そのバリデーションを、バリデート対象の Construct の持つ node インスタンスの addValidation メソッドでアタッチします。
export class MyConstruct extends Construct { constructor(scope: Construct, id: string) { super(scope, id); // ... // ... this.node.addValidation(new Validator(someParam)); // クラスを作成しなくても良い this.node.addValidation({ validate: () => this.validateMyArray(someParam) }); } private validateMyArray(someParam: string): string[] { const errors = []; // ... return errors; } }
バリデーションの遅延実行
このバリデーションの特徴は、バリデーションの処理が遅延実行される点です。つまり、Construct フェーズでコードが実行されている最中にはバリデーションは実行されず、Validate フェーズでまとめて実行されます。
例えば、Construct に myVariables という配列を用意しておき、さらに要素を myVariables に追加する addVariable というメソッドを用意しておくとします。つまり、Construct のインスタンスを生成した後にそのメソッドを呼び出すことで、myVariables に要素を追加できるようにします。
export class MyConstruct extends Construct { private myVariables: string[] = []; constructor(scope: Construct, id: string) { super(scope, id); } public addVariable(variable: string) { this.myVariables.push(variable); } } const myConstruct = new MyConstruct(this, 'MyConstruct'); myConstruct.addVariable('var1'); myConstruct.addVariable('var2'); myConstruct.addVariable('var3'); myConstruct.addVariable('var4');
このようなケースで、インスタンス生成時でなく、addVariable メソッドを複数回呼び出すことで要素を追加した後にバリデーションを実行したい場合に、バリデーションの遅延実行が役立ちます。
constructor(scope: Construct, id: string) { super(scope, id); // ... //... this.node.addValidation({ validate: () => this.validateVariables() }); } private validateVariables(): string[] { const errors: string[] = []; if (this.myVariables.length > 2) { errors.push(`myVariables must not have more than 2 elements, got: ${this.myVariables.length}`); } return errors; }
この場合、myVariables に 4 つの要素が追加された後にバリデーションが実行されるため、以下のようなエラーメッセージが表示されます。
Error: Validation failed with the following errors: [MyStack/MyConstruct] myVariables must not have more than 2 elements, got: 4
より具体的な説明は、以前 builders.flash に寄稿した 「AWS CDK におけるバリデーションの使い分け方を学ぶ」という記事に記載していますので、そちらもご参照ください。
Synthesize フェーズ

CloudFormation テンプレートの生成
このフェーズでは、Construct ツリーをもとに CloudFormation テンプレートが生成されます。
具体的には、ツリーの中の L1 Construct を抽出し、それらをもとに CloudFormation リソースを生成していきます。

またこのフェーズでは以下の処理も行われ、CloudFormation テンプレートに反映されます。
- トークンの解決
- 論理 ID の計算
addOverrideなどを使用したプロパティの上書き
クラウドアセンブリの生成
そして、CloudFormation テンプレートを含むクラウドアセンブリが cdk.out ディレクトリに生成されます。
クラウドアセンブリの構成要素(アーティファクト)は以下の通りです。
- CloudFormation テンプレート ({スタック名}.template.json)
- アセットファイル
- Dockerfile
- Lambda コード
- その他 S3 にアップロードするファイル
- その他
ライフサイクルの昔話
Prepare や Validate、Synthesize フェーズは Construct が持つ prepare 、validate 、synthesize というメソッドを実行するフェーズでした。今でも公式ドキュメントにはそう書いてあります。
しかし、それらのメソッドは CDK v1 時代のもので、現在の Construct 構造にはもうありません。そもそもライフサイクルのフェーズ自体が CDK v1 時代に定義されたもので、当時の CDK 内の実装の構造をフェーズとして表したものです。
現在の CDK 内の実装は当時と比べてかなり変更されていますが、現在でもライフサイクルの概念自体は残っており、フェーズの名前もそのまま使われています。
AWS CDK の仕組み
今回の内容は、以前登壇した『AWS CDK の仕組み』というセッションの内容がもとになっています。もしよろしければ、そちらのスライドもご参照ください。
最後に
AWS CDK のアプリケーションライフサイクルについて解説しました。ライフサイクルの各フェーズでどのような処理が行われているかを理解することで、CDK アプリケーションの動作をより深く理解できるようになります。