🦔

AWS CDK で「デプロイ前に違反を発見しブロックする」仕組みに関する考察と、プラグインによる検証手法の提案

2024/07/14に公開

Abstract

CDK に限らず、一般的な IaC あるいはアプリケーションのデプロイにおいて「デプロイする前に気づきたい」という用事はちょいちょいあります。こと CDK においては、できれば cdk synth あるいは cdk deploy コマンドを実行した際に、やりたいバリデーションが必ず実行されるようにしておき、バリデーションが通らないならば synth, deploy コマンド自体も失敗させることで確実に「違反」をブロックしたいところです。

CDK には複数のバリデーションの手段が存在します。それらを「デプロイ実行前に確実にブロックしたい」というモチベにおいて、どう使えるか考えてみます。

この記事では、まずは CDK が提供する仕組みを使って「デプロイに検証して違反をブロックする」を実現する、一般的な手段を紹介し、次にプラグインを自作することで synth の結果を直接検証する方法を提案(紹介)します。

前提事項

自分の手元の開発環境において、commit する前や開発環境へのデプロイ前など、できるだけ開発サイクルの手前の段階で検知したいし、違反は漏れなくブロックして synth や deploy 自体が正常に実行できないようにしたい、ということが今回の前提となります。

CDK のバージョンは v2.149.0 に準拠します。

AWS CDK を使って「デプロイする前に違反をブロックする」仕組み

まず、CDK で実装するバリデーションに関する概論は、別の方が執筆した記事で既に完成度の高い情報が存在します。次の記事が非常にわかりやすく、よくまとまっていますので、詳しくはそちらをご覧ください。

AWS CDK におけるバリデーションの使い分け方を学ぶ - 後藤 健太 (AWS DevTools Hero)
https://aws.amazon.com/jp/builders-flash/202406/cdk-validation/

概論としてこの記事でも簡単に紹介します(上記の記事を通読している方は、本セクションは流してもらってOKです)。

CDK に組み込まれている仕組みだと、だいたい次のよう手段を使う人が多いのではないかと思います。

  1. IValidation を実装した自作のバリデーションを任意の Construct に適用する
  2. 自作の Aspects を実装し、バリデーションに利用する
  3. テスト で検出する

これ以外にも、自前の Construct 定義の中で直接値を検証して即例外をスローする、というやり方もあります。実際にはこの方法が StackProps の検証をはじめとして最も出番が多いでしょう。筆者の見解では、この方法と IValidation の実装によるチェックが CDK 的に最もスタンダード、かつ頻出のパターンだと思います。両者の方法は synth, deploy コマンドの結果をブロックしてくれますので、この点も今回の前提となるモチベ(デプロイ前に確実にブロックしたい)に適っています。

例外の即時スローと IValidation についてはそれほど解説することはないので、それ以外の方法についての私見を以降で述べます。

自作 Aspects を実装する方法

Aspects をバリデーションに応用する場合は CDK アプリケーションのライフサイクルを意識する必要があり、少々落とし穴的な要素があります。Aspects をポリシー検証の用途で使う場合の懸念点の多くは、おそらくここに起因する話が多いように思います。CDK アプリケーションのライフサイクルについては以下のドキュメントを参照してください。

https://aws.amazon.com/jp/builders-flash/202406/cdk-validation/

もう少し具体的な難点を挙げると以下の2点になります。

  • 他の Aspects で実装した処理の適用を検証することが難しい
  • 遅延解決を必要とする値を扱えない場合がある

前者は、実装する検証の仕組み自体も Aspects であることからほぼ自明です。Aspects の適用順序をコントロールする確実かつ明瞭な方法があれば話は別ですが、少なくとも筆者はそのような方法を知りません。また、Construct のツリー構造の中での適用順序と、同一 Construct の中での適用順序の2つを考慮する必要があるため、仮に Aspects の適用順序を制御できる方法があったとしても、今後保守する人間からするとややこしい・取り扱い注意な仕組みになってしまい、あまり嬉しくないように思います。

後者は、CDK アプリケーションのライフサイクルの中で実行される仕組みを採用した以上はどうにもなりません。例外の即時スローや IValidation による実装方法も同様のことが言えます。

これを回避するためには、CDK アプリケーションのライフサイクルの外で機能する仕組みを採用する必要があります。具体的には、synth が実行された後の出力を検証する方法や、assertion モジュールの機能を使ってテストの枠組み(後述)で検出するアプローチを用いる必要があります。

ここまでが Aspects を直接的にバリデーションに利用する場合の難点です。ただし、前者については実装の仕方で回避することが可能です。Aspects 自体をバリデーションに利用するのではなく、IValidation の適用対象を選別するための手段として利用する方法があり、Aspect 自体でバリデーション処理の実行するのに代えてこの方法を用いるのが良いと思います。

以下に実装例を提示します。自前の Aspects の visit() メソッドで適用対象をフィルタし、対象に選ばれた Construct に対してのみ addValidation を呼び出すように実装しています。

import { IAspect } from 'aws-cdk-lib';
import { IValidation, IConstruct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';

class MyValidation implements IValidation {
  constructor(private node: ec2.CfnSecurityGroup) {}

  public validate(): string[] {
    // CfnSecurityGroup 型の this.node に対する何らかのバリデーションを実装。
    return [];
  }
}

class MyValidationApplyManager implements IAspect {
  public visit(node: IConstruct): void {
    // 特定のリソースタイプの場合にのみバリデーションを適用するために IAspect を利用する実装例。
    // ここでは SecurityGroup に対してバリデーションを適用する例を提示。
    if (node instanceof ec2.CfnSecurityGroup) {
      node.node.addValidation(new MyValidation(node));
    }
  }
}

const app = new cdk.App();
const stack = new MyStack(app, 'MyStack1');

cdk.Aspects.of(app).add(new MyValidationApplyManager());

このような実装をすることで、スコープ内の特定条件を満たす Construct に一括で同じバリデーションを適用できます。汎用的に使えるので、この方法は割とおすすめです。

テストで検出する

この方法は CDK が提供する Template クラスを利用します。ほぼ生の CloudFormation (JSON) を触ることになるので、使用感は synth の出力結果を検証するのに近いものになります。

テストの枠組みをバリデーションに利用することに関して筆者が考える最大の利点は、synth の出力結果に限りなく近いものを扱える点です。遅延解決が必要な値ができるだけ解決された後の値を扱えるのが、例外の即時スローや Aspects, IValidation を用いたバリデーションにはできない利点となります。CDK アプリケーションのライフサイクル内で実行する検証手段は、その検証対象によっては Lazy な値しか扱えず、そもそも具象値の検証自体が不可能、というケースもあります。

しかし、「デプロイする前に違反を検知し、ブロックできる」という観点において、この方法はいくつかの欠点があります。

まず、直接 cdk synth, deploy コマンドを実行した場合は検査の仕組みがスルーされてしまうこと。CI に組み込んだりするなどして、デプロイ前にテストが必ず動作してくれるような仕組みを追加することが前提になります。コミットする前の手元の開発環境でもチェックしておきたい、ということであれば、npm scripts で自前のデプロイやビルドを定義してそれを利用したり、git hook で pre-commit や pre-push を定義するなどの方法があろうかと思います。これらは充分現実的かつ一般的な方法ですが、いずれにせよ、「確実にブロックする」という視点では追加の仕組みがないと機能しません。[1]

もう1つの問題は、自分で定義した Construct の外部で行われた処理を含めた最終的な結果の検証が難しいことです。例えば App スコープを含む複数の Construct に Aspects を適用し、最終的に Aspects がきちんと意図した通りに適用されたかどうかを検証したいケースが該当します。

// app.ts
const app = cdk.App();
const stack = MyStack(app, 'MyStack');

class MyAspects implements cdk.IAspect {
  public visit(node: IConstruct): void {
    // do something awesome
    console.log(`Visiting ${node.node.path}`);
  }
}

class MyStack extends cdk.Stack {
  // ...
}

// 自作の Aspect を CDK App 全体に適用
cdk.Aspects.of(app).add(new MyAspects(app));

CDK で普通にユニットテストを書く場合は、だいたい以下のような書き方をテンプレ的に使うことが多いと思います。

import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';

test('unit testing', () => {
  const app = new cdk.App();
  const stack = new MyStack(app, 'MyTestStack');
  const t = Template.fromStack(stack);
  // do something assertions by using methods of Template class
});

このテストコードにおけるテスト対象は MySatck ということになりますが、対して自作の Aspects はその Stack の外側で、親スコープの App に対して適用されています。上記のテストコードはテスト対象の Stack の外側にある実行パスを通過していないので、テストコードそのものが自作自演になってしまいます。

App スコープに対して適用する処理も含めて確実に検証したいという用事がある場合、テストコードの枠組みではこれを確実に保証することは難しくなります。

自作した検証の仕組みが確実に機能するようにしておかないと、実装者のケアレスミスで簡単に検証の仕組みが素通りされてしまいます。これは保守の観点において余計な考慮事項またはトラップを増やすことになり、あまり嬉しくありません。

自作プラグインによるバリデーション実装の提案

ここまでで、一般的な CDK におけるバリデーションの方法について整理してきました。

実は、cdk synth の段階でブロックできる仕組みとして機能する手段が CDK にはもう1つ存在します。それが「プラグイン」の実装です。

この方法は AWS が公式ドキュメント(下記)を出していますが、ざっと検索してみた感じではこれを利用した実装について解説している情報源はほとんどなかったので、おそらくまだニッチな方法なのだと思います。ただ、筆者の視点としてはこの方法が有用なシーンはありそうだと思いましたので、今回取り上げてみることにしました。

AWS CDK policy validation at synthesis time
https://docs.aws.amazon.com/cdk/v2/guide/policy-validation-synthesis.html

プラグインは、IPolicyValidationPluginBeta1 インタフェースを実装した自作クラスを用意することで実装できます。それを App の初期化時に渡すことで適用します。以下のリファレンスにも使用方法の例が記載されています。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib-readme.html#policy-validation

また、AWS Blog からもこのプラグイン機構を利用したバリデーションの実装アプローチとして CfnGuardValidator が紹介されています。

https://aws.amazon.com/blogs/mt/accelerating-development-with-aws-cdk-plugin-cfnguardvalidator/

自作プラグインは IPolicyValidationPluginBeta1 を実装する必要があります。具体的には、オブジェクトは以下のメソッドを備える必要があります。

public validate(context: IPolicyValidationContextBeta1): PolicyValidationPluginReportBeta1

パラメータの型である IPolicyValidationContextBeta1 は次のような定義を持ちます。

export interface IPolicyValidationContextBeta1 {
    /**
     * The absolute path of all templates to be processed
     */
    readonly templatePaths: string[];
}

templatePaths は JSON 形式の CloudFormation ファイルの絶対パスです。このテンプレートは cdk synth の成果物の一部です(通常 cdk.out/ の下に出力される内容です)。つまり、このプラグインは synth の出力結果となる CloudFormation テンプレートを直接検証するための手段として位置づけることができます。

戻り値の PolicyValidationPluginReportBeta1 は次のような定義です。

export interface PolicyValidationPluginReportBeta1 {
    /**
     * List of violations in the report.
     */
    readonly violations: PolicyViolationBeta1[];
    /**
     * Whether or not the report was successful.
     */
    readonly success: boolean;
    /**
     * The version of the plugin that created the report.
     * @default - no version
     */
    readonly pluginVersion?: string;
    /**
     * Arbitrary information about the report.
     *
     * @default - no metadata
     */
    readonly metadata?: {
        readonly [key: string]: string;
    };
}

PolicyViolationBeta1 の詳しい型定義はドキュメント等を参照いただくとして、バリデーションの戻り値型としてなんとなくイメージは理解できる定義内容かと思います。プラグインはバリデーションの結果情報としてこの型を返す必要があります。

このプラグイン実装のアプローチの特徴は、大きく2つあります。

  1. synth した結果の CloudFormation テンプレートファイルを入力とする仕組みである
  2. このプラグインによって検出された「違反」は、cdk synth, cdk deploy コマンドを失敗させる

まず1の観点です。基本的に JSON ファイルをそのまま扱うことになるため、TypeScript の恩恵はほぼ受けられません。自分で JSON 構造をパースして目的の検証対象を掘っていく必要があります。しかし、synth した後の結果を取り扱う仕組みであることから「遅延解決」が必要な値を対象とするバリデーションにもかなり広範囲に対応できる点が利点となります。

次に、2の観点です。実装したインタフェースで非 success な結果を返せば、cdk synth, deploy コマンドは失敗します。つまり「デプロイ前に違反をブロックする」という用事が確実に達成できることになります。

CDK のバリデーションはあくまで作りたいモノに対する副次的な仕組みであるため、開発・保守の担当者が構築用のコード変更を行う際に特別に意識を払わずとも機能するような仕組みであると嬉しい、と筆者は考えています。CDK CLI の標準的な動作に組み込める仕組みとして利用できることが利点であると考えます。

まとめると、プラグインによるバリデーション実装は、以下のような特徴を利点として評価できる場合に採用できるアプローチであると言えます。

  1. 標準的なデプロイ方法 (cdk synth, cdk deploy) の実行サイクルに組み込める
  2. 最終的に生成されるテンプレートに対する検証を実装できる
      1. 遅延解決が必要な値に対しても機能する
      2. エントリポイントで実行する設定もひっくるめた最終的な synth の結果に対する検証ができる

プラグインの実装例

具体的に前述の特徴に当てはまるようなユースケースってなんなのよ、という話をします。

プラグインによるアプローチが有効と思われるケースのひとつが「リソースタグに付与必須のキーが存在し、それをデプロイ前に検査したい」になります。発端は筆者が所属会社の方で執筆した以下の記事内容になります。

https://blog.serverworks.co.jp/2024/07/12/204810

上記記事の考察より、リソースタグは基本的に Aspects で追加/削除する実装になるため即時スローや Aspects を使ったアプローチは使えず、また IValidation による方法も基本的に遅延解決が必要な値を除外する前提を付けないと機能しない、ということがわかっています。また、assertion モジュールの Template クラスを使ったテストベースの手法は、そもそもリソースタグの付与という処理自体を Stack の外で行うことが普通にありえるためテストで検証したい実行パスをカバーできる保証がない点、そして CDK の標準的なデプロイの手順の外にある仕組みであるため追加の仕組みを用意しないと「デプロイ前に検知してブロック」というモチベが果たされない点がいまひとつと述べています。そのあたりの指摘は本記事でも先に述べた通りです。

前置きはほどほどにして、早速実装例を提示します。あんまり汎用的な作りではないと思いますし、設計はいまひとつだと思います。複数のルールを適用するプラグインを作りたい場合にこの実装はあまり参考にならないと思います。そのあたりの実装のイケてなさ具合はご容赦ください。

この実装例では、「タグ付け可能な全リソースに対して、特定のタグキーの付与を義務付けるポリシー」を検証する自作プラグインを実装しています。

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct, IConstruct, IValidation } from 'constructs';
import path from 'path';
import fs from 'fs';

// ==============================
// Utils
// ==============================
function getVersion(): string {
  const packageJsonPath = path.resolve(path.join(__dirname, 'package.json'));
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));

  return packageJson.version as string;
}

// ==============================
// Plugin
// ==============================
type CfnResourceTag = {Key: string, Value: string};

/**
 * タグ付け可能な全リソースに対して必須タグキーの付与を義務付けるポリシーを検証するための自作プラグイン
 */
class MyPlugin implements cdk.IPolicyValidationPluginBeta1 {
  static readonly MY_PLUGIN_INSPECT_TAG = 'cdk:user:MyPlugin:inspect';
  static readonly VERSION = getVersion();
  static readonly RULE_MISSING_REQUIRED_TAG_KEY = 'RULE_MISSING_REQUIRED_TAG_KEY';
  public readonly name = 'MyPlugin';
  private scope?: IConstruct;

  constructor(private requiredTagKeys: string[]) {}

  public bind(scope: IConstruct): void {
    this.scope = scope;

    cdk.Tags.of(scope).add(MyPlugin.MY_PLUGIN_INSPECT_TAG, 'enabled');
  }

  public validate(context: cdk.IPolicyValidationContextBeta1): cdk.PolicyValidationPluginReportBeta1 {
    const results = context.templatePaths.map(templatePath => this.validateTemplate(templatePath)).flat();
    const success = results.length === 0;

    return {
      success: success,
      pluginVersion: MyPlugin.VERSION,
      violations: results,
    };
  }

  private validateTemplate(templatePath: string): cdk.PolicyViolationBeta1[] {
    const template = JSON.parse(fs.readFileSync(templatePath, 'utf-8'));
    const resources = template.Resources as Record<string, any>;

    const results: cdk.PolicyViolationBeta1[] = [];
    (Object.entries(resources)).forEach(([logicalId, resource]) => {
      // const resourceType = resource.Type as string;
      const properties = resource.Properties as Record<string, any>;
      const tags = properties.Tags as CfnResourceTag[] | undefined;

      if (!tags) { return; }
      const shouldInspect = tags.some(tag => tag.Key === MyPlugin.MY_PLUGIN_INSPECT_TAG && tag.Value === 'enabled');
      // console.log(`Inspecting resource ${logicalId} of type ${resourceType}: tags=${JSON.stringify(tags)}`);
      if (!shouldInspect) { return; }

      // implement your validation logic here
      for (let requiredTagKey of this.requiredTagKeys) {
        if (!tags.some(tag => tag.Key === requiredTagKey)) {
          results.push({
            ruleName: MyPlugin.RULE_MISSING_REQUIRED_TAG_KEY,
            description: `Resource ${logicalId} does not have required tag ${requiredTagKey}`,
            violatingResources: [{
              resourceLogicalId: logicalId,
              templatePath,
              locations: [`Resources/${logicalId}/Properties/Tags`],
            }],
          });
        }
      }
    });

    return results;
  }
}

// ==============================
// Stack
// ==============================
class MyStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, 'MyVpc', {
      maxAzs: 2,
    });

    const sg = new ec2.SecurityGroup(this, 'MySecurityGroup', {
      vpc,
    });

    // あまりよろしくない Security Group の定義。今回の例示にはあんまり関係ないのでスルーしてOK
    sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'Violated rule');
  }
}

// ==============================
// CDK App
// ==============================
// 'Owner' タグを必須要求する
const plugin = new MyPlugin(['Owner']);

const app = new cdk.App({
  // 自作プラグインを追加
  policyValidationBeta1: [
    plugin,
  ],
});

// app スコープ内部の Construct から適用対象をマークする(検査用のリソースタグを付与する)ための処理
plugin.bind(app);

cdk.Tags.of(app).add('Owner', 'Alice', {
  // タグ付けのポリシーに違反するリソースをわざと用意するための設定。
  // ここでは VPC に対して必須タグを付けなかったケースを想定した。
  excludeResourceTypes: ['AWS::EC2::VPC'],
});

const stack = new MyStack(app, 'MyStack');

上記の実装のポイントは以下の通りです。

  1. プラグインの検査対象をマークするためにリソースタグを利用する
  2. タグの付与が可能なすべての Construct に対して検査対象をマークするためのリソースタグ Key=cdk:user:MyPlugin:inspect, Value=enabled を付与し、このタグが付与されたリソースをバリデーション対象として扱う
  3. App リソースの初期化時に MyPlugin を渡し、その後適用対象を認識する(=特定用のリソースタグをマークする)ための Aspects を追加するための手段 (bind メソッド) を用意し、実行する

リソースタイプによってはタグをサポートしないものも存在しうるので、検査時に「タグを付与しなかったリソース」と「仕様上タグが付与できないリソース」を区別できる必要がありました。この課題を解決するために、プラグインが検査対象とするリソースを明示的にマークする仕組みを入れることにしました。具体的には、Aspects を使って App スコープ内のタグ付け可能なリソースに対して「検査対象であること」をマークする専用のリソースタグを追加するアプローチを採用しました。[2]

実際に cdk synth コマンドを実行してみます。なお、上記の実装例はわざと AWS::EC2::VPC タイプのリソースだけ必須タグを付与しないようにしています。cdk synth を実行すると、VPC リソースでポリシー違反が検出されるはずです。

$ npx cdk synth -a 'npx ts-node main.ts' -q --debug
Performing Policy Validations

Validation Report
-----------------

╔══════════════════════╗
║    Plugin Report     ║
║   Plugin: MyPlugin   ║
║   Version: 1.0.0     ║
║   Status: failure    ║
╚══════════════════════╝


(Violations)

RULE_MISSING_REQUIRED_TAG_KEY (1 occurrences)

  Occurrences:

    - Construct Path: MyStack/MyVpc/Resource
    - Template Path: cdk.out/MyStack.template.json
    - Creation Stack:
        └──  MyStack (MyStack)
             │ Construct: aws-cdk-lib.Stack
             │ Library Version: 2.149.0
             │ Location: Object.<anonymous> (/home/hassaku63/example-project/main.ts:107:15)
             └──  MyVpc (MyStack/MyVpc)
                  │ Construct: aws-cdk-lib.aws_ec2.Vpc
                  │ Library Version: 2.149.0
                  │ Location: new MyStack (/home/hassaku63/example-project/main.ts:11:17)
                  └──  Resource (MyStack/MyVpc/Resource)
                       │ Construct: aws-cdk-lib.aws_ec2.CfnVPC
                       │ Library Version: 2.149.0
                       │ Location: new Vpc (/home/hassaku63/example-project/node_modules/aws-cdk-lib/aws-ec2/lib/vpc.js:1:11555)
    - Resource ID: MyVpcF9F0CA6F
    - Template Locations:
      > Resources/MyVpcF9F0CA6F/Properties/Tags

  Description: Resource MyVpcF9F0CA6F does not have required tag Owner

Policy Validation Report Summary

╔══════════╤═════════╗
║ Plugin   │ Status  ║
╟──────────┼─────────╢
║ MyPlugin │ failure ║
╚══════════╧═════════╝

Validation failed. See the validation report above for details

Subprocess exited with error 1

このように、cdk synth の実行自体が失敗します。一見見づらく感じるかもしれませんが、どのリソースで違反が検出されたのかはきちんと提示されていますし、必要十分な情報が出力できているように思います。

これで、「実際にデプロイに使用する設定内容で synth した結果を検証できる」かつ「標準的なデプロイ方法に組み込める、漏れなく動作する "違反検知" の仕組み」この2点を両立する仕組みを作ることができました。

実装例に関する注意点

「検査対象をマークするタグ」というアイデアですが、リソースタグを検証する用事以外なら基本的にはそこまで必要じゃないと思います。考えなしにリソースタグを追加することは避けた方が良いでしょう。

ただ、あるリソースタイプの中でも検査の適用対象かどうかが分かれるようなケースでは、こうした仕組みがあった方が実装はわかりやすくなるように思います。筆者が思いつく範囲で例示するなら「Public Subnet に配置したリソースにのみ適用する検査」のような要件ではこの例示のようなマーキングのためのタグ仕様の導入は検討価値があると思います。[3]

リソースタグは付与できる数に上限があります[4]。もしこの方法を複数のルールで採用する必要が生じた場合は、値を JSON Encode するなどしてプラグインの情報全部を1個のタグキーに押し込むなどの実装を考慮する必要があるでしょう。

まとめ

「デプロイ前に検査したい」「検知が漏れにくい仕組みにしたい」という用事を満たすために、どういった方法が採れるのか、筆者なりの考察を述べました。また、プラグインの実装という方法を提示し、この方法が他の(一般的な)バリデーションの手法とは異なるユースケースに対して適用可能であることを示しました。

「デプロイ前に発見する」方法が充実している点が、筆者が CDK を推す最大の理由のひとつであり、また CDK の面白さだと思います。この記事でそのあたりが伝わると良いなと思います。

この利点は自由度の高さだとか選択肢の広さという観点にも繋がるわけですが、技術選定のシーンではそうした多様さをネガティブに評価せざるを得ない場合もあります。それでも、アプリケーションとインフラの開発・保守を兼任する体制が普通のチームでは、こうした CDK の特徴を「良さ」として活かせる機会は多いのではないでしょうか。手札のひとつとして、ここで提示したものをなにか一つでも覚えておいてもらえると筆者としては嬉しいです。中でも IValidation と Aspects を組み合わせた方法はかなり汎用性があり、筆者としても自信をもっておすすめできるアプローチなので、ぜひ持ち帰っていただけると嬉しいです。

繰り返しますが、プラグイン実装の手法は現時点でそこまで強く推奨はしません。CDK の枠組みの中だけでもバリデーションの手段としてよく知られた方法が複数存在しますし、そもそもすべてをデプロイ前に発見せねばならないということもありません。Config 等を利用した発見的統制でも実務上問題ないのでは?という視点も考慮したうえで、最終的にプラグイン実装でなければ満たせない用事であると判断した場合にのみ採用すると良いでしょう。

脚注
  1. 筆者は、CI の仕組みや git hook を使って担保する方法自体は、どちらも現実解として妥当な方法だと考えています。ただ、今回の記事の本旨として「できるだけ追加の仕組みを導入しないでよく、かつ手元の開発環境の開発サイクルの中でも機能する方法を考えたいよね」という想定で書いているため、これを課題点として扱いました ↩︎

  2. 実際の利用シーンでは「特定条件を満たすリソースにのみルールを適用したい」という要求もありそうですので、その場合はそもそもこの提案実装のような bind という手段は不適切です。CDK App の実装者自身が Aspects を用いて、プラグイン実装の外で「検査対象としてマークするタグ」の付与を適切にコントロールしてもらう方針で設計し、プラグイン側はその内部表現(タグ値の encode/decode 方法など)を隠蔽する手段を提供する設計とした方が綺麗かもしれません。 ↩︎

  3. もちろん、生の JSON を力技でパースして "Public Subnet に紐づくリソースである" ことを判別する実装でも、同等のことが可能です。が、その分 any を多く扱うコードになりますし、多分実装そのそのも煩雑だと思います ↩︎

  4. https://docs.aws.amazon.com/tag-editor/latest/userguide/tagging.html#tag-conventions ↩︎

Discussion