AWS CDK に追加された Property Injectors の使い方

AWS CDK v2.196.0 で Property Injectors という機能が導入されました。

目次

目次


Property Injectors とは?

App や Stack、Construct 内の、「特定の種類の L2 Construct(一部の L3 Construct も含む)」のすべてに対して、プロパティ(props)を一元的に書き換えるといった機能です。AWS CDK v2.196.0で導入されました。


詳細としては、こちらのRFCに説明があります。

※こちらのPRで追加されました。(本当はその前に別の PR で追加されたがそちらは Revert された)


例えば、スタック内のすべての S3 バケットに対して、「blockPublicAccessBLOCK_ALLにしたい」というようなコンプライアンス要件があったとしましょう。

Property Injectors を使うと、以下のように実現できます。


まずは、IPropertyInjectorを implements するMyBucketPropsInjectorを作成します。

this.constructUniqueIdには対象の L2 Construct が公開するPROPERTY_INJECTION_IDを指定し、injectメソッドでは新しい props を返すようにします。

export class MyBucketPropsInjector implements IPropertyInjector {
  public readonly constructUniqueId: string;

  constructor() {
    this.constructUniqueId = Bucket.PROPERTY_INJECTION_ID;
  }

  public inject(originalProps: BucketProps, _context: InjectionContext): BucketProps {
    return {
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
      enforceSSL: true,
      ...originalProps,
    };
  }
}


そして、以下のようないくつかの実装方法で、適用したいスコープにMyBucketPropsInjectorを指定します。これで、指定したスコープ内のバケットblockPublicAccessを一括でBLOCK_ALLにすることができます。

const app = new App();
const stack = new Stack(app, 'MyStack', {
  propertyInjectors: [new MyBucketPropsInjector()],
});
const app = new App();

PropertyInjectors.of(app).add(new MyBucketPropsInjector());


Aspects との比較

Aspects との違い

上記のような要件を実現するために、従来は Aspects などを使用していたのではないでしょうか。

Aspects でそのようなことを行う際は、Aspects 内で対象の L1 Construct が所有するプロパティを直接、もしくはaddPropertyOverrideなどを使用して書き換えていました。

今回登場した「Property Injectors」ですが、Construct のプロパティではなく、Construct の props を直接書き換えるという点で Aspects と異なります。

実際は Property Injectors の内部で新しい props を生成して適用するため、props で定義されたプロパティにreadonly識別子がついていようと問題なく書き換えられます。


ただし Property Injectors はあくまでも L2 や L3 Construct の props を書き換えるため、Aspects と違って L1 Construct を対象にすることはできません。


また、Aspects は CDK アプリケーションのライフサイクルでは 2 番目の「Prepare フェーズ」で実行されます。


つまり Aspects は、全ての Construct の作成が終わった後に Construct ツリーを走査して実行されます。

しかし、Property Injectors では props を直接書き換えるため、Construct の生成のタイミング(ライフサイクルの 1 番目である「Construct フェーズ」)で値の書き換えが適用されます。


そのため次のように、適用したい Stack の生成の後にPropertyInjectors.of().addによって Property Injectors を適用してしまうと、書き換え処理が実行されません。

const app = new cdk.App();

new MyStack(app, 'MyStack', props);

PropertyInjectors.of(app).add(new MyBucketPropsInjector());


以下のように、Stack の生成より先に呼び出すか、もしくは App や Stack の props のpropertyInjectorsに指定しましょう。

const app = new cdk.App();

PropertyInjectors.of(app).add(new MyBucketPropsInjector());

new MyStack(app, 'MyStack', props);

// Or
new MyStack(app, 'MyStack', {
  propertyInjectors: [new MyBucketPropsInjector()],
});


Aspects との使い分け

Aspects の使い分け方として、個人的な観点では以下のようになるのではと思います。(まだあまり使い慣れていないため、後で違う観点が出てくるかもしれません。)

Property Injectors が向いているケース:

  • Construct の持つプロパティや CloudFormation のプロパティの粒度ではなく、L2 Construct の props の粒度で書き換えたいケース
  • L2 Construct 内の組み込みバリデーションがパスするようにプロパティを書き換えたいケース
    • = Aspects が実行される前にエラーが起きるケース

Aspects が向いているケース:

  • L1 も含む Construct を書き換えたいケース
  • CloudFormation のプロパティの粒度でプロパティを書き換えたいケース
  • リソースのプロパティを一元的に検査したい(書き換えはしない)ケース


テクニック紹介

例えば Property Injectors で S3 バケットの中にserverAccessLogsBucket(これも S3 バケットの型)を作成したいとしましょう。

これをただ先ほど紹介したような実装をしてしまうと、無限ループになってしまいます。

それを避けるには、以下のようなスキップ処理をする必要があります。

export class SpecialBucketInjector implements IPropertyInjector {
  public readonly constructUniqueId: string;

  // this variable will track if this Injector should be skipped.
  private _skip: boolean;

  constructor() {
    this._skip = false;
    this.constructUniqueId = Bucket.PROPERTY_INJECTION_ID;
  }

  public inject(originalProps: BucketProps, context: InjectionContext): BucketProps {
    if (this._skip) {
      return originalProps;
    }

    let accessLogBucket = originalProps.serverAccessLogsBucket;
    if (!accessLogBucket) {
      // When creating a new accessLogBucket, disable further Bucket injection.
      this._skip = true;

      // Since injection is disabled, make sure you provide all the necessary props.
      accessLogBucket = new Bucket(context.scope, 'my-access-log', {
        blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
        removalPolicy: originalProps.removalPolicy ?? core.RemovalPolicy.RETAIN,
      });

      // turn on injection for Bucket again.
      this._skip = false;
    }

    return {
      serverAccessLogsBucket: accessLogBucket,
      ...originalProps,
    };
  }
}


最後に

L2 Construct のプロパティを一元的に書き換えられる Property Injectors の紹介でした。使い所は結構あると思います。