[CDKv2]CloudFrontへWAF適応にパラメータストアを使う。


CloudFrontにWAFを当てる場合、WAFv2ではWAFをus-east-1リージョンに作成しないといけない。
CloudFrontが他のリージョンのリソースと一緒に定義されている場合、WAF適応に指定するARNをパラメータストア経由で渡したくなるが、そのままでは別リージョンのパラメータストアにはアクセスできない。

`cdk-remote-stack`というモジュールで対応できるみたいだが、Lambda@Edgeを噛ませた場合にうまくいかなかったので以下を参考にCDKv2でクロスリージョンでのSSMパラメータストア読み書きを行うクラスを用意した。

https://stackoverflow.com/questions/59774627/cloudformation-cross-region-reference

```
$ cdk --version     
2.1.0 (build f4f18b1)
```

```ts:lib/xorigin-ssm-parameter-accessor.ts
import { Construct } from 'constructs';
import { AwsCustomResource, AwsCustomResourcePolicy, AwsSdkCall, PhysicalResourceId } from 'aws-cdk-lib/custom-resources';
interface SSMParameterReaderProps {
  action: "getParameter" | "putParameter" | "deleteParameter";
  parameters: {
    Name: string;
    Value?: string;
    Description?: string;
    Type?: string;
    Overwrite?: boolean;
  }
  region: string;
}

export class XOriginSSMParameterAccessor extends AwsCustomResource {
  constructor(scope: Construct, name: string, props: SSMParameterReaderProps) {
    const {action, parameters, region} = props;

    const ssmAwsSdkCall: AwsSdkCall = {
      service: "SSM",
      action,
      parameters,
      region,
      physicalResourceId: PhysicalResourceId.of(Date.now().toString()),
    };
    super(scope, name, { 
      onUpdate: ssmAwsSdkCall,
      policy: AwsCustomResourcePolicy.fromSdkCalls({
        resources: AwsCustomResourcePolicy.ANY_RESOURCE
      })
    });
  }

  public getParameterValue(): string {
    return this.getResponseField("Parameter.Value").toString();
  }
}
```

`onUpdate`(リソースの更新時)で指定のAWS-SDKを実行する。ここでは他のリージョンのパラメータストアから読み出す処理。Lambdaが実行環境となるのでそのポリシーも指定するみたい。

```ts:lib/waf-stack.ts
import { Construct } from 'constructs';
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
import * as ssm from 'aws-cdk-lib/aws-ssm';

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

    // == const ==
    const wafName = "YOUR_WAF_NAME";

    // == WAF ==
    const myIpSet = new wafv2.CfnIPSet(this, "WhiteIPList", {
      name: "WhiteIPList",
      addresses: ["***.***.***.***/32"],
      ipAddressVersion: "IPV4",
      scope: "CLOUDFRONT",
    });

    const myWaf = new wafv2.CfnWebACL(this, wafName, {
      defaultAction: { block: {} },
      name: wafName,
      rules: [
        // aws managed rule
        {
          priority: 1,
          overrideAction: { none: {} },
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: `${wafName}-AWSManagedRulesKnownBadInputsRuleSet`,
          },
          name: `${wafName}-AWSManagedRulesKnownBadInputsRuleSet`,
          statement: {
            managedRuleGroupStatement: {
              vendorName: "AWS",
              name: "AWSManagedRulesKnownBadInputsRuleSet"
            }
          }
        },
        // custom rule
        {
          priority: 2,
          action: { allow: {} },
          name: `${wafName}-CustomAllowIpSetRule`,
          statement: {
            ipSetReferenceStatement: {
              arn: myIpSet.attrArn
            },
          },
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: `${wafName}-CustomAllowIpSetRule`,
          },
        },
      ],
      scope: "CLOUDFRONT",
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        metricName: `${wafName}`,
        sampledRequestsEnabled: true
      }
    });

    // == export ==
    new ssm.StringParameter(this, 'SSMParamWafArn', {
      parameterName: "/cdk-params/wafArn",
      stringValue: myWaf.attrArn,
    });
  }
}
```

```ts:lib/cloudfront-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import { XOriginSSMParameterAccessor } from './xorigin-ssm-parameter-accessor';

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

    // == import ==
    const wafAttrArnReader = new XOriginSSMParameterAccessor(this, `SSMParameterReader`, {
      action: "getParameter",
      parameters: {
        Name: "/cdk-params/wafArn",
      },
      region: 'us-east-1',
    });

    // == CloudFront ==
    new cloudfront.Distribution(this,"Distribution", {
      webAclId: wafAttrArnReader.getParameterValue(),
      // 他略
    });
  }
}
```

```ts:bin/main.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CloudFrontStack } from '../lib/cloudfront-stack';
import { WafStack } from '../lib/waf-stack';

const app = new cdk.App();
new WafStack(app, 'WafStack', {
    env: {
        account: "************",
        region: "us-east-1",
    }
})
new CloudFrontStack(app, 'CloudFrontStack', {
    env: {
        account: "************",
        region: "ap-northeast-1",
    }
});
```