はじめに
皆さん、こんにちは。AWS DevTools Hero の後藤と申します。普段、AWS Cloud Development Kit (AWS CDK) へのコントリビュート活動を行っており、Top Contributor、並びに Community Reviewer に選定いただいています。
AWS CDK は、使い慣れたプログラミング言語でクラウドリソースを定義できるオープンソースのフレームワークです。
今回は AWS CDK で、複数のリソースのプロパティに対して、一元的に・一括で設定を適用する TIPS 集をご紹介します。
builders.flash メールメンバー登録
builders.flash メールメンバー登録で、毎月の最新アップデート情報とともに、AWS を無料でお試しいただけるクレジットコードを受け取ることができます。
プロパティの一括適用 TIPS
今回ご紹介するプロパティの一括適用 TIPS は、以下の 3 つの機能を用いたものになります。
- Aspects
- PropertyInjectors
- RemovalPolicies
1. Aspects
最初にご紹介するのは、Aspects です。
Aspects とは、あるスタックやコンストラクトといった、任意のスコープ内の全てのリソースに同じ操作を適用するための機能です。
1-1. Aspects の使い方
以下のように、IAspect というインターフェースを実装したクラスを定義し、visit メソッドに任意の種類のリソースに対するプロパティの上書き処理を記述することで、複数のリソースのプロパティを一括で適用することができます。
// Aspects は IAspect インターフェースを実装することで実現する
export class BucketVersioningUpdater implements cdk.IAspect {
public visit(node: IConstruct): void {
// Aspects によって渡された Construct の内部のリソース全てに visit メソッドが適用されるので、CfnBucket リソースの場合にのみ実行されるよう条件分岐をする
if (node instanceof CfnBucket) {
// バケットのバージョニング設定が無効の場合は有効に上書きする
if (
!node.versioningConfiguration ||
(!cdk.Tokenization.isResolvable(node.versioningConfiguration) &&
node.versioningConfiguration.status !== 'Enabled')
) {
node.addPropertyOverride('VersioningConfiguration', {
Status: 'Enabled',
});
}
}
}
}
const app = new cdk.App();
const stack = new CdkSampleStack(app, 'CdkSampleStack');
// CdkSampleStack 内の全てのリソースに対して BucketVersioningUpdater を適用
cdk.Aspects.of(stack).add(new BucketVersioningUpdater());
1-2. Aspects の参考記事
Aspects の詳細な使い方は、AWS 公式ブログの CDK Aspects を利用してベストプラクティスに従ったインフラストラクチャを構築する からもご紹介されていますので、ぜひご覧ください。
また、Aspects はプロパティの上書きだけでなく、プロパティの検証 (バリデーション) としても使用できます。Aspects でプロパティの検証をする方法については、以前私が builders.flash に寄稿した AWS CDK におけるバリデーションの使い分け方を学ぶ という記事で解説しているので、こちらも参考にしてみてください。
1-3. 複数の Aspects の実行順序の調整
同じスコープに対して複数の Aspects を適用する場合、priority を指定することで、それぞれの Aspects の実行順序を調整することができます。
こちらは AWS CDK の v2.172.0 で導入されたプロパティです。
priority には数値を指定し、数値が小さいほど優先度が高くなり先に実行されます。また `AspectPriority` という列挙型のように使えるクラスも用意されており、static 変数として MUTATING (=200)、DEFAULT (=500)、READONLY (=1000) が選択できます。
ただし、複数の Aspects に対して同じ priority の値を指定した場合はそれらの実行順序が保証されないため、それぞれの Aspects には違う値を指定することをお勧めします。
1-3-1. priority の使用例
例えば以下のように、1 つのスタックに対して複数の Aspects を適用したとします。これは、最初にプロパティの上書き処理を行い、最後にプロパティの検証を行うという例です。この場合、AspectForOverride → AspectForOther1 → AspectForOther2 → AspectForValidation の順序で実行されます。
const app = new cdk.App();
const stack = new CdkSampleStack(app, 'CdkSampleStack');
// priority = 1000
cdk.Aspects.of(stack).add(new AspectForValidation(), { priority: cdk.AspectPriority.READONLY });
// priority = 200
cdk.Aspects.of(stack).add(new AspectForOverride(), { priority: cdk.AspectPriority.MUTATING });
// priority = 500
cdk.Aspects.of(stack).add(new AspectForOther1(), { priority: cdk.AspectPriority.DEFAULT });
// priority = 600
cdk.Aspects.of(stack).add(new AspectForOther2(), { priority: 600 });
1-3-2. cdk-nag での優先度設定
cdk-nag のような、Aspects によって CDK スタックの検証を行うライブラリを使用する場合にも、その優先度を低くしておく(priority の数値を大きくする)ことで他の Aspects が実行された後に検証を行うといった使い方ができます。
cdk-nag の具体的な使い方に関しては、AWS 公式ブログの AWS Cloud Development Kit と cdk-nag でアプリケーションのセキュリティとコンプライアンスを管理する という記事をご覧ください。
2. PropertyInjectors
次にご紹介するのは、PropertyInjectors です。こちらは、2025 年 5 月に AWS CDK の v2.196.0 で導入された機能です。
PropertyInjectors は、あるスコープ内において、任意の種類の L2 Construct (一部の L3 Construct も含む) のすべてのリソースに対して props を一元的に書き換える機能です。詳細については、AWS CDK の Developer Guide にある CDK ブループリントを使用してコンストラクトを設定する をぜひご覧ください。
※上記の Developer Guide では「CDK ブループリント(CDK Blueprints)」というアプローチとして紹介されていますが、本記事では他に紹介する機能 (Aspects, RemovalPolicies) と同様に CDK の実際のクラス名である PropertyInjectors という名称で解説を統一します。
2-1. PropertyInjectors の使い方
例えば、スタック内のすべての Amazon S3 のバケットに対して、「blockPublicAccess を BLOCK_ALL にしたい」という要件があったとしましょう。こちらの要件に対して、PropertyInjectors を使った例をご紹介します。
2-1-1. IPropertyInjector を実装するクラスを作成
IPropertyInjector を implements する MyBucketPropsInjector クラスを作成します。constructor 内の this.constructUniqueId に上書きしたい L2 Construct が公開する PROPERTY_INJECTION_ID を指定し、inject メソッドには任意のプロパティを上書きした新しい props を返すようにします。
export class MyBucketPropsInjector implements IPropertyInjector {
public readonly constructUniqueId: string;
constructor() {
// Bucket コンストラクトが公開する PROPERTY_INJECTION_ID を指定
this.constructUniqueId = Bucket.PROPERTY_INJECTION_ID;
}
// 任意のプロパティを上書きした新しい props を返す
public inject(originalProps: BucketProps, _context: InjectionContext): BucketProps {
return {
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
...originalProps,
};
}
}
2-1-2. App や Stack への適用
以下のように適用したいスコープに MyBucketPropsInjector を追加します。これにより、スコープ内のすべてのバケットの blockPublicAccess を一括で BLOCK_ALL に上書きすることができます。
const app = new App();
// App に指定する場合
PropertyInjectors.of(app).add(new MyBucketPropsInjector());
// 特定のスタックに指定する場合
new Stack(app, 'MyStack', {
propertyInjectors: [new MyBucketPropsInjector()],
});
2-2. 無限ループの回避
PropertyInjectors で Bucket の props に、同じく Bucket の型である serverAccessLogsBucket を設定したいとします。
PropertyInjectors は、対象の Construct の生成タイミングで実行されます。
そのためこの要件に対して上記で紹介した実装をすると、serverAccessLogsBucket のために新しい Bucket を作成しようとした時に、再度 PropertyInjectors が呼び出されます。そして、その Bucket の中でも serverAccessLogsBucket を作成する処理が実行され、無限ループが生じてエラーが発生してしまいます。
それを避けるには、次に紹介するようなスキップ処理を実装することで無限ループを回避することができます。
2-2-1. 無限ループ回避の実装例
PropertyInjectors で無限ループを回避するための実装例を示します。
export class SpecialBucketInjector implements IPropertyInjector {
public readonly constructUniqueId: string;
// スキップするか判断するためのフラグ
private _skip: boolean;
constructor() {
this._skip = false;
this.constructUniqueId = Bucket.PROPERTY_INJECTION_ID;
}
public inject(originalProps: BucketProps, context: InjectionContext): BucketProps {
// スキップフラグが true の場合は、元のプロパティをそのまま返す
if (this._skip) {
return originalProps;
}
let accessLogBucket = originalProps?.serverAccessLogsBucket;
if (!accessLogBucket) {
// Construct 生成のタイミングで新しく PropertyInjectors が実行されるため、
// ここでスキップフラグを true にしておく
this._skip = true;
// スキップフラグが true なのでこのインスタンスではプロパティの上書きが行われない
// つまり、この Bucket に対しては `serverAccessLogsBucket` が上書きされない
accessLogBucket = new Bucket(context.scope, 'my-access-log', {
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
});
// スキップフラグを false に戻す
this._skip = false;
}
return {
serverAccessLogsBucket: accessLogBucket,
...originalProps,
};
}
}
2-3. Aspects と PropertyInjectors の違い
Aspects と PropertyInjectors は、どちらも任意のスコープ内の複数のリソースに対してプロパティを一括で適用できますが、いくつかの違いがあります。
- 適用対象の違い
- 適用タイミングの違い
2-3-1. 適用対象の違い
Aspects は L1 Construct の設定プロパティを上書きするのに対し、PropertyInjectors は L2 もしくは L3 Construct の props 自体を書き換えるという点が大きな違いです。
2-3-2. 適用タイミングの違い
Aspects は、CDK アプリケーションのライフサイクルのうち 2 番目の「Prepare フェーズ」で実行されます。
基本的には Construct の大部分が 1 番目の「Construct フェーズ」で実行されます。つまり Aspects は、CDK アプリケーションの大部分の Construct の作成が終わった後に Construct ツリーを走査して実行されます。
一方で、PropertyInjectors は L2 もしくは L3 Construct の props を直接書き換えるため、Construct の生成のタイミング (ライフサイクルの 1 番目である「Construct フェーズ」) で値の書き換えが適用されます。
※CDK アプリケーションのライフサイクルに関しては、先ほどご紹介した「AWS CDK におけるバリデーションの使い分け方を学ぶ」記事にて詳しく解説しているのでそちらをご覧ください。
2-4. Aspects と PropertyInjectors の使い分け
Aspects と PropertyInjectors の使い分け方としては、以下のようなケースが考えられます。
Aspects が向いているケース:
- 書き換え対象のリソースに L1 Construct を使用しているケース
- AWS CloudFormation のプロパティの粒度で書き換えを行いたいケース
- リソースのプロパティを一元的に検査したい(上書きはしない)ケース
PropertyInjectors が向いているケース:
- L1 Construct (CloudFormation) のプロパティの粒度ではなく、L2 もしくは L3 Construct の props の粒度でより抽象的に書き換えたいケース
- L2 Construct 内の組み込みバリデーションで起こるエラーを防ぐためにプロパティを書き換えたいケース (Aspects が実行されるのは Construct の生成が完了した後なので、PropertyInjectors でないと L2 Construct 内でのエラーを防止できない)
2-5. PropertyInjectors の適用タイミングの注意点
先ほどご説明したように、PropertyInjectors は、適用対象の Construct が生成されたタイミングで実行されます。
2-5-1. PropertyInjectors の適用タイミングの問題例
次のように、適用したい Stack の生成の後に PropertyInjectors.of().add によって PropertyInjectors を適用しても、書き換え処理が実行されません。
const app = new App();
new CdkSampleStack(app, 'CdkSampleStack');
// ここで PropertyInjectors を適用しても、CdkSampleStack の生成後なので書き換え処理は実行されない
PropertyInjectors.of(app).add(new MyBucketPropsInjector());
2-5-2. PropertyInjectors の適用タイミングの対処法
以下のように、Stack の生成より先に呼び出すか、もしくは App や Stack の props の propertyInjectors に指定しましょう。
const app = new App();
// スタックの生成より先に PropertyInjectors を適用する
PropertyInjectors.of(app).add(new MyBucketPropsInjector());
new CdkSampleStack(app, 'CdkSampleStack');
// もしくは スタックの props に指定する
new CdkSampleStack(app, 'CdkSampleStack', {
propertyInjectors: [new MyBucketPropsInjector()],
});
3. RemovalPolicies
3 つ目にご紹介するのは、RemovalPolicies です。こちらは AWS CDK の v2.183.0 で導入された機能です。
RemovalPolicies を使用すると、任意のスコープ内のすべてのリソースに対して、一括で RemovalPolicy を適用できます。
3-1. RemovalPolicies の使い方
例えばすべてのリソースの RemovalPolicy を一元的に設定したい場合、以下のように記述します。
const app = new App();
// すべてのリソースの RemovalPolicy を DESTROY に設定
RemovalPolicies.of(app).destroy();
// すべてのリソースの RemovalPolicy を RETAIN に設定
RemovalPolicies.of(app).retain();
3-2. MissingRemovalPolicies
すでに L2 Construct の内部であらかじめ RemovalPolicy が設定されているリソースや、自分で明示的に RemovalPolicy を設定しているリソースもあるかと思います。そのようなリソースを除いたすべてのリソースに対して一括で RemovalPolicy を適用したい場合には、MissingRemovalPolicies が使用できます。
const app = new App();
// RemovalPolicy がまだ設定されていないリソースに対して DESTROY に設定
MissingRemovalPolicies.of(app).destroy();
最後に
AWS CDK で以下の機能を利用して、複数のリソースのプロパティに対して一元的に・一括で設定を適用する TIPS 集をご紹介しました。
- Aspects
- PropertyInjectors
- RemovalPolicies
これらの機能を活用することで、複数のリソースのプロパティを効率的に管理し、コードの可読性や保守性を向上させることができます。
筆者プロフィール
後藤 健太 (AWS DevTools Hero / @365_step_tech)
AWS CDK のコントリビュート活動を行っており、Top Contributor や Community Reviewer に選定。2024 年 2 月に発足されたコミュニティ駆動の CDK コンストラクトライブラリである Open Constructs Library では、メンテナーを担っている。
また、cls3 や delstack といった自作 AWS ツールの OSS 開発も行なっている。2024 年 3 月、AWS DevTools Hero に選出。