AWS WAFv2をCDK(Typescript)で構築してみた(2022年9月版)

昔書いたAWS WAFv2のCloudFormationテンプレートをCDKで書いてみました
2022.09.29

みなさん、こんにちは。

AWS事業本部コンサルティング部の芦沢(@ashi_ssan)です。

CDKにこれまで全く触れていなかったのですが業務でCDKを扱う必要が出てきたため、最近CDKの勉強を始めました。

本エントリでは、以前作成したAWS WAFv2を作成するCloudFormationテンプレートをCDKで書いてみます。

なお、執筆者である私はインフラエンジニア出身でType Scriptをほとんど書いたことがありません。お手柔らかにコードを見ていただけると助かります。フィードバック大歓迎です。

同僚のたかくにから以下のAWS公式ブログの存在を教えてもらったことが本エントリを執筆するきっかけになりました。感謝です。

元ネタ

今回のCDKコードの元ネタになっているCloudFormationテンプレートはこちらのブログで紹介しているものです。

事前準備

事前にWAFに関連付けするALB等のリージョナルリソースが必要になります。

検証環境がない方はこちらのブログを参考すると、ALBが含まれるバックエンド環境をCDKで作成できるのでおすすめです。

作成できたら、作成したALBのARNをメモしておいてください。

CDKで書いてみた

今回利用するCDKのコードはこちらのGitHubリポジトリに上げています。

本エントリでは、lib/cdk_wafv2-stack.tsについて紹介します。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

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

    // Define Variable
    const Prefix = 'devio';
    const Env = 'develop';
    const Scope = 'REGIONAL';
    const WebAclAssociationResourceArn = 'arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/devio-stg-alb/xxxxxxxxxxxxxx';

    // Define Resource Name
    const WebAclName = Env + '-' + Prefix + '-web-acl';
    const S3ForWaflogName = 'aws-waf-logs-' + Env + '-' + Prefix + '-' + cdk.Stack.of(this).account;
    const S3ForAthenaQuery = 'athena-query-results-' + Env + '-' + Prefix + '-' + cdk.Stack.of(this).account;

    // S3 Config (Restricted Public Access)
    const public_access_block_config : cdk.aws_s3.CfnBucket.PublicAccessBlockConfigurationProperty = {
      blockPublicAcls: true,
      blockPublicPolicy: true,
      ignorePublicAcls: true,
      restrictPublicBuckets: true,
    };

    // S3
    const cfnS3ForWaflog = new cdk.aws_s3.CfnBucket(this, "S3BucketForWaflogConfig", {
      bucketName: S3ForWaflogName,
      publicAccessBlockConfiguration: public_access_block_config,
    })

    const cfnS3ForAthenaQuery = new cdk.aws_s3.CfnBucket(this, "S3BucketForAthenaQueryConfig", {
      bucketName: S3ForAthenaQuery,
      publicAccessBlockConfiguration: public_access_block_config
    })

    // WAF
    const cfnWebACL = new cdk.aws_wafv2.CfnWebACL(this,WebAclName,{
        defaultAction: {
          allow: {}
        },
        scope: Scope,
        visibilityConfig: {
          cloudWatchMetricsEnabled: true,
          metricName: WebAclName,
          sampledRequestsEnabled: true,
        },
        name: WebAclName,
        rules: [
          {
            name: 'AWS-AWSManagedRulesCommonRuleSet',
            priority: 0,
            statement: {
              managedRuleGroupStatement: {
                name:'AWSManagedRulesCommonRuleSet',
                vendorName:'AWS',
                excludedRules: [
                  {name: 'SizeRestrictions_BODY'},
                  {name: 'NoUserAgent_HEADER'},
                  {name: 'UserAgent_BadBots_HEADER'},
                  {name: 'SizeRestrictions_QUERYSTRING'},
                  {name: 'SizeRestrictions_Cookie_HEADER'},
                  {name: 'SizeRestrictions_BODY'},
                  {name: 'SizeRestrictions_URIPATH'},
                  {name: 'EC2MetaDataSSRF_BODY'},
                  {name: 'EC2MetaDataSSRF_COOKIE'},
                  {name: 'EC2MetaDataSSRF_URIPATH'},
                  {name: 'EC2MetaDataSSRF_QUERYARGUMENTS'},
                  {name: 'GenericLFI_QUERYARGUMENTS'},
                  {name: 'GenericLFI_URIPATH'},
                  {name: 'GenericLFI_BODY'},
                  {name: 'RestrictedExtensions_URIPATH'},
                  {name: 'RestrictedExtensions_QUERYARGUMENTS'},
                  {name: 'GenericRFI_QUERYARGUMENTS'},
                  {name: 'GenericRFI_BODY'},
                  {name: 'GenericRFI_URIPATH'},
                  {name: 'CrossSiteScripting_COOKIE'},
                  {name: 'CrossSiteScripting_QUERYARGUMENTS'},
                  {name: 'CrossSiteScripting_BODY'},
                  {name: 'CrossSiteScripting_URIPATH'}
                ]
              }
            },
            visibilityConfig: {
              cloudWatchMetricsEnabled: true,
              metricName:'AWS-AWSManagedRulesCommonRuleSet',
              sampledRequestsEnabled: true,
            },
            overrideAction: {
              none: {}
            },
          }
        ]
      });

      // Export WAF logs to S3
      const webAclAssociation = new cdk.aws_wafv2.CfnWebACLAssociation(this,"webAclAssociation",
        {
          resourceArn: WebAclAssociationResourceArn,
          webAclArn: cfnWebACL.attrArn,
        }
      )
      webAclAssociation.addDependsOn(cfnWebACL)

      // Resource Associations
      const cfnLoggingConfiguration = new cdk.aws_wafv2.CfnLoggingConfiguration(this, 'CfnLoggingConfiguration',
        {
          logDestinationConfigs: [cfnS3ForWaflog.attrArn],
          resourceArn: cfnWebACL.attrArn,
        });
  }
}

各箇所について、それぞれ説明していきます。


こちらがCloudFormationテンプレートのParametersの箇所にあたるところです。わかりやすいように変数として指定してみました。

WebAclAssociationResourceArnの変数内のARNはダミーを入力しています。実際にCDKを利用する際は検証環境のALB ARNを入力してください。

    // Define Variable
    const Prefix = 'devio';
    const Env = 'develop';
    const Scope = 'REGIONAL';
    const WebAclAssociationResourceArn = 'arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/devio-stg-alb/xxxxxxxxxxxxxx';


ここでS3の設定を行なっています。

WAFログ保存用のバケットとAthenaクエリ保存用のバケット1を作成します。

どちらもパブリックアクセスは不要なため、public_access_block_configでパブリックアクセスをすべて禁止する設定にしました。

    // S3 Config (Restricted Public Access)
    const public_access_block_config : cdk.aws_s3.CfnBucket.PublicAccessBlockConfigurationProperty = {
      blockPublicAcls: true,
      blockPublicPolicy: true,
      ignorePublicAcls: true,
      restrictPublicBuckets: true,
    };

    // S3
    const cfnS3ForWaflog = new cdk.aws_s3.CfnBucket(this, "S3BucketForWaflogConfig", {
      bucketName: S3ForWaflogName,
      publicAccessBlockConfiguration: public_access_block_config,
    })

    const cfnS3ForAthenaQuery = new cdk.aws_s3.CfnBucket(this, "S3BucketForAthenaQueryConfig", {
      bucketName: S3ForAthenaQuery,
      publicAccessBlockConfiguration: public_access_block_config
    })


ここが今回のメインのAWS WAFのWeb ACLに関する箇所です。

基本的にはCloudFormationテンプレートと同じ感覚で書くことができました。

excludedRulesの箇所についてはサブルール1つごとに{}で閉じる必要があるため注意です(私はそこでハマってしまいました)

    // WAF
    const cfnWebACL = new cdk.aws_wafv2.CfnWebACL(this,WebAclName,{
        defaultAction: {
          allow: {}
        },
        scope: Scope,
        visibilityConfig: {
          cloudWatchMetricsEnabled: true,
          metricName: WebAclName,
          sampledRequestsEnabled: true,
        },
        name: WebAclName,
        rules: [
          {
            name: 'AWS-AWSManagedRulesCommonRuleSet',
            priority: 0,
            statement: {
              managedRuleGroupStatement: {
                name:'AWSManagedRulesCommonRuleSet',
                vendorName:'AWS',
                excludedRules: [
                  {name: 'SizeRestrictions_BODY'},
                  {name: 'NoUserAgent_HEADER'},
                  {name: 'UserAgent_BadBots_HEADER'},
                  {name: 'SizeRestrictions_QUERYSTRING'},
                  {name: 'SizeRestrictions_Cookie_HEADER'},
                  {name: 'SizeRestrictions_BODY'},
                  {name: 'SizeRestrictions_URIPATH'},
                  {name: 'EC2MetaDataSSRF_BODY'},
                  {name: 'EC2MetaDataSSRF_COOKIE'},
                  {name: 'EC2MetaDataSSRF_URIPATH'},
                  {name: 'EC2MetaDataSSRF_QUERYARGUMENTS'},
                  {name: 'GenericLFI_QUERYARGUMENTS'},
                  {name: 'GenericLFI_URIPATH'},
                  {name: 'GenericLFI_BODY'},
                  {name: 'RestrictedExtensions_URIPATH'},
                  {name: 'RestrictedExtensions_QUERYARGUMENTS'},
                  {name: 'GenericRFI_QUERYARGUMENTS'},
                  {name: 'GenericRFI_BODY'},
                  {name: 'GenericRFI_URIPATH'},
                  {name: 'CrossSiteScripting_COOKIE'},
                  {name: 'CrossSiteScripting_QUERYARGUMENTS'},
                  {name: 'CrossSiteScripting_BODY'},
                  {name: 'CrossSiteScripting_URIPATH'}
                ]
              }
            },
            visibilityConfig: {
              cloudWatchMetricsEnabled: true,
              metricName:'AWS-AWSManagedRulesCommonRuleSet',
              sampledRequestsEnabled: true,
            },
            overrideAction: {
              none: {}
            },
          }
        ]
      });


最後にログ出力設定とWAFへのリソース関連付け設定の箇所についてです。

こちらもCloudFormationと同じ感覚で書いていました。

組み込み関数を利用する必要がないので、コードがシンプルだと感じます。

      // Export WAF logs to S3
      const webAclAssociation = new cdk.aws_wafv2.CfnWebACLAssociation(this,"webAclAssociation",
        {
          resourceArn: WebAclAssociationResourceArn,
          webAclArn: cfnWebACL.attrArn,
        }
      )
      webAclAssociation.addDependsOn(cfnWebACL)

      // Resource Associations
      const cfnLoggingConfiguration = new cdk.aws_wafv2.CfnLoggingConfiguration(this, 'CfnLoggingConfiguration',
        {
          logDestinationConfigs: [cfnS3ForWaflog.attrArn],
          resourceArn: cfnWebACL.attrArn,
        });

説明は以上です。

CDKでデプロイ/削除してみる

こちらのGitHubリポジトリに今回のCDKコードをアップロードしています再掲します。Cloneしてご利用ください。

git clone https://github.com/h-ashisan/aws-cdk-create-wafv2-2022-9.git

Cloneしたディレクトリ配下に移動して、パッケージをインストールします

npm install

こちらのコマンドでデプロイしてください

cdk deploy

正常終了すると、マネジメントコンソール側(CloudFormation、WAF)でも正常にリソースが作成できていることが確認できます。


削除する際には、WAFログ用のS3バケット(aws-waf-logs-develop-devio-123456789012)の中身を事前に空にする必要がありますので注意です。

その後、以下コマンドで削除できます。

cdk destroy

最後に

ガッツリとCDKに触れたのは今回が初めてでしたが、L1の抽象度で書いたおかげなのかCloudFormartionテンプレートと同じ感覚でコード書くことができた印象が強いです。

CDKを利用することによって、CloudFormationテンプレートにはなかった「コードのシンプルさ」「デプロイ/削除の簡単さ」が体験できました。

今後もCDKでいろんなサービスを構築してみたいと思いました。

以上、AWS事業本部コンサルティング部の芦沢(@ashi_ssan)でした。

参考


  1. 元ネタのCloudFormationテンプレートでは、後の手順でAthenaテーブルを作成して分析を行うため、Athena用のバケットも作成しています