AWS CDK v2でAspectを使ってアーキテクチャのコンプライアンス準拠をチェックする方法


はじめに

おはようございます、加藤です。
AWS CDKでクラウドアプリケーションを開発するためのベストプラクティスというAWSブログではコンプライアンスのためにConstructを使わないという事が推奨されています。

コンプライアンスに基づいてConstructをラップしてしまうと、AWS Solutions Constructsなどが公開している便利なライブラリが利用できなくなるからという理由です。

代わりに組織での SCP や permission boundary の使い方を調べて、セキュリティガードレールを適用しましょう。また aspects や CFN Guardのようなツールを使用して、デプロイ前にインフラストラクチャのプロパティに関するアサーションを行いましょう。

ブログ内でコンプライアンス準拠の手段としていくつか紹介されていますが、そのうちAspectを使った方法をこのブログではご紹介します。

Aspectとは

ドキュメントによるとAspectは特定のスコープ内のすべての構成に操作を適用する方法です。タグを追加するなどして構成を変更したり、すべてのバケットが暗号化されていることを確認するなど、構成の状態について何かを検証したりできます。

ドキュメントでは下記のようなサンプルコードも公開されており、デザインパターンのVisitorパターンで実装されているようです。紹介されているのはCDK v1のコードなので以降の手順ではv2向けに置き換えつつ行います。

class BucketVersioningChecker implements IAspect {
  public visit(node: IConstruct): void {
    // See that we're dealing with a CfnBucket
    if (node instanceof s3.CfnBucket) {

      // Check for versioning property, exclude the case where the property
      // can be a token (IResolvable).
      if (!node.versioningConfiguration 
        || (!Tokenization.isResolvable(node.versioningConfiguration)
            && node.versioningConfiguration.status !== 'Enabled')) {
        Annotations.of(node).addError('Bucket versioning is not enabled');
      }
    }
  }
}

// Later, apply to the stack
Aspects.of(stack).add(new BucketVersioningChecker());

Aspectでコンプライアンス準拠を検証する

S3バケットに対してバージョン管理を行うコンプライアンスがあると仮定してそれに準拠できているかを検証します。

まず、バージョン管理が設定されているか検証するライブラリを書きます。

lib/bucket-versioning-checker.ts
import {CfnBucket} from 'aws-cdk-lib/aws-s3';
import {Annotations, IAspect, Tokenization} from 'aws-cdk-lib';
import {IConstruct} from 'constructs';

/**
 * Verify that the S3 bucket configures for versioning.
 */
export class BucketVersioningChecker implements IAspect {
  private readonly fix: boolean;

  constructor(props?: {fix?: boolean}) {
    this.fix = props?.fix ?? false;
  }

  public visit(node: IConstruct) {
    if (node instanceof CfnBucket) { // ①
      if (
        !node.versioningConfiguration ||
        (!Tokenization.isResolvable(node.versioningConfiguration) &&
          node.versioningConfiguration.status !== 'Enabled')  // ②
      ) {
        if (this.fix) {
          node.addPropertyOverride('VersioningConfiguration', {
            Status: 'Enabled',
          });
        } else {
          Annotations.of(node).addError('Bucket versioning is no enabled'); // ③
        }
      }
    }
  }
}

① visitメソッドにはすべてのConstructが流れ込んできます。そのためS3バケットのConstructだけを抽出する必要があります。

② バージョン管理が無効であること(式としては有効になっていないこと)を確認します。ただこれだけですが、node.versioningConfigurationの型がCfnBucket.VersioningConfigurationProperty | cdk.IResolvable | undefinedなので型を絞り込むためにTypeGuard関数や比較をしています。

③ Constructのメタデータにエラーを追加します。エラーを含むConstructが存在するとシンセサイズする際に失敗するためデプロイを防ぐ事ができます。

バージョニングを行っていないバケットを作成します。

lib/cdk-aspect-demo-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

export class CdkAspectDemoStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    new Bucket(this, 'Bucket');
  }
}

ライブラリを使用しつつStack定義を呼び出します。

bin/cdk-aspect-demo.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import * as CdkAspectDemo from '../lib/cdk-aspect-demo-stack';
import * as lib from '../lib/bucket-versioning-checker';

const app = new cdk.App();
const cdkAspectDemoStack = new CdkAspectDemo.CdkAspectDemoStack(
  app,
  'CdkAspectDemoStack'
);

cdk.Aspects.of(cdkAspectDemoStack).add(new lib.BucketVersioningChecker());

Aspects.ofでスタック(上位のConstruct)を指定し作成したライブラリをaddすることで利用ができます。

デプロイコマンドを実行するとBucket versioning is no enabledとメッセージが表示されて、デプロイが停止されます。

cdk deploy

# [Error at /CdkAspectDemoStack/Bucket/Resource] Bucket versioning is no enabled

# Found errors

テスト実行時にエラーを検出する

デプロイ前にコンプライアンスしたがってエラーを出力し停止してくれるのは便利ですが、そもそもマージする前にエラーを検出し修正してからマージできるとなお便利です。

test/cdk-aspect-demo.test.ts
import { BucketVersioningChecker } from '../lib/bucket-versioning-checker';
import { Template } from 'aws-cdk-lib/assertions';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Aspects, App, Stack } from 'aws-cdk-lib';

describe('bucket versioning', () => {
  test('バージョニングが有効になっている', () => {
    const app = new App();
    const stack = new Stack(app, 'test-stack');
    new Bucket(stack, 'bucket', { versioned: false });

    Aspects.of(stack).add(new BucketVersioningChecker());

    const assembly = app.synth();
    const { messages } = assembly.getStackArtifact(stack.artifactId);

    expect(messages).toHaveLength(0);
  });
});

単純にテスト内でAspectsを呼び出すだけではエラーが検出されません。なので、スタックのメタデータを確認しエラーが記録されていたらテスト失敗とします。Received arrayの内容を確認すると、messages[0].entryにエラーが記録されていることがわかります。

npm run test

 FAIL  test/cdk-aspect-demo.test.ts (7.469 s)
  ✕ aspect test (43 ms)

  ● aspect test

    expect(received).toHaveLength(expected)

    Expected length: 0
    Received length: 1
    Received array:  [{"entry": {"data": "Bucket versioning is no enabled", "trace": ["Annotations.addMessage (/$WORKDIR/cdk-aspect-demo/node_modules/aws-cdk-lib/core/lib/annotations.ts:106:9)", "Annotations.addError (/$WORKDIR/cdk-aspect-demo/node_modules/aws-cdk-lib/core/lib/annotations.ts:76:5)", "BucketVersioningChecker.visit (/$WORKDIR/cdk-aspect-demo/lib/bucket-versioning-checker.ts:27:32)", "recurse (/$WORKDIR/cdk-aspect-demo/node_modules/aws-cdk-lib/core/lib/private/synthesis.ts:123:9)", "recurse (/$WORKDIR/cdk-aspect-demo/node_modules/aws-cdk-lib/core/lib/private/synthesis.ts:156:23)", "recurse (/$WORKDIR/cdk-aspect-demo/node_modules/aws-cdk-lib/core/lib/private/synthesis.ts:156:23)", "recurse (/$WORKDIR/cdk-aspect-demo/node_modules/aws-cdk-lib/core/lib/private/synthesis.ts:156:23)", "invokeAspects (/$WORKDIR/cdk-aspect-demo/node_modules/aws-cdk-lib/core/lib/private/synthesis.ts:107:11)", "Object.synthesize (/$WORKDIR/cdk-aspect-demo/node_modules/aws-cdk-lib/core/lib/private/synthesis.ts:35:16)", "App.synth (/$WORKDIR/cdk-aspect-demo/node_modules/aws-cdk-lib/core/lib/stage.ts:178:38)", "Object.<anonymous> (/$WORKDIR/cdk-aspect-demo/test/cdk-aspect-demo.test.ts:12:24)", "Object.asyncJestTest (/$WORKDIR/cdk-aspect-demo/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:106:37)", "/$WORKDIR/cdk-aspect-demo/node_modules/jest-jasmine2/build/queueRunner.js:45:12", "new Promise (<anonymous>)", "mapper (/$WORKDIR/cdk-aspect-demo/node_modules/jest-jasmine2/build/queueRunner.js:28:19)", "/$WORKDIR/cdk-aspect-demo/node_modules/jest-jasmine2/build/queueRunner.js:75:41", "processTicksAndRejections (node:internal/process/task_queues:96:5)"], "type": "aws:cdk:error"}, "id": "/test-stack/bucket/Resource", "level": "error"}]

      13 |   const { messages } = assembly.getStackArtifact(stack.artifactId);
      14 |
    > 15 |   expect(messages).toHaveLength(0);
         |                    ^
      16 | });
      17 |

      at Object.<anonymous> (test/cdk-aspect-demo.test.ts:15:20)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        7.514 s, estimated 9 s
Ran all test suites.

{ versioned: true }に変更するとテストOKに変わることも確認してみてください。

パラメーターを上書きする

Aspectではパラメーターを上書きすることもできます。作成したライブラリはプロパティーに{ fix: true }が与えられるとバージョニングを有効化します。

test/cdk-aspect-demo.test.ts
import { BucketVersioningChecker } from '../lib/bucket-versioning-checker';
import { Template } from 'aws-cdk-lib/assertions';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Aspects, App, Stack } from 'aws-cdk-lib';

describe('bucket versioning', () => {
  test('バージョニングを有効にする', () => {
    const app = new App();
    const stack = new Stack(app, 'test-stack');
    new Bucket(stack, 'bucket', { versioned: false });

    Aspects.of(stack).add(new BucketVersioningChecker({ fix: true }));

    const assembly = app.synth();
    const { messages } = assembly.getStackArtifact(stack.artifactId);

    expect(messages).toHaveLength(0);

    const template = Template.fromStack(stack);
    template.hasResource('AWS::S3::Bucket', {
      Type: 'AWS::S3::Bucket',
      UpdateReplacePolicy: 'Retain',
      DeletionPolicy: 'Retain',
      Properties: {
        VersioningConfiguration: {
          Status: 'Enabled',
        },
      },
    });
  });
});

あとがき

今回のコードはこちらに公開しております。

intercept6/cdk-aspect-demo

Aspectを使うことでConstructのコンプライアンスを検証および上書きによる準拠が行えます。この方法であればAWSやサードパーティによって配布されているパターンConstructを自分たちにとって最適であるか検証および最適に変更することが簡単におこなえます。

以上でした。