S3 + CloudFront + WAFv2 をCDKで構築した時にregion等でハマった話

「CloudFront経由でS3にアクセスする + WAFv2でアクセス制限をかける」リソース一式を構築するAWS CDKのサンプルです．

- WAFv2リソースは`us-east-1`リージョンのスタックとして
- S3バケットとCloudFrontは`それ以外の`リージョンのスタックとして(検証ではap-northeast-1を使用)

deployする方法と,実装でハマったポイントをまとめました．
CDKは，2021.04.13時点での最新バージョンである，1.98.0を使用しました．

## 動機

- S3でホスティングしている既存ウェブサイト(ap-northeast-1)に独自ドメインを割り当てたかったが，バケット名がドメイン名と異なっており，CloudFrontの導入が必要となった．

- CloudFront経由でのアクセスに，IPベースでのアクセス制限をかける必要があり，WAFでのアクセス制御も必要となった．

- 既存リソースはCDKで管理されており，新たに追加するリソースもCDKの管理下にしたかった．

## Overview

[CDK サンプルコード](https://github.com/nasubeee/cdkstudy-cloudfront-s3-hosting)

![architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495439/a9e6149f-a181-1534-d186-12214a754b61.png)

## 01-waf stack

WAFv2 WebACLを管理するスタックです．

[CloudFormation - WAFv2 - Scope](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-webacl.html#aws-resource-wafv2-webacl-properties)に書かれているように，WAFv2のACLをCloudFrontに関連付けたい場合，US East (N. Virginia) Region (us-east-1)へWAFv2リソースを作成する必要があります．そこで，[app.ts](./bin/app.ts)で明示的にリージョンを指定しています．

```ts
const wafStack = new WafStack(app, '01-waf', {
  // (省略)
  env: {
    account: stackEnv.account,
    region: "us-east-1", // ここ
  },
  // (省略)
});
```

実装当初，この制限に気づかず，deployエラーでハマりました．(WAFv1の場合，リージョン制限は無いようです．)
後述のスタックで作成するCloudFrontとの関連付けには，作成したWebACLのARNが必要となります．
ここでは，SSM Parameter StoreにWebACL ARNを格納しました（これを参照する方法は後述）．

## 02-web-dist stack

- コンテンツを格納するS3バケット
- S3バケットを向き先としたCloudFront distribution

を管理するスタックです．技術検証のため，01-waf スタックと別リージョンにしています．

S3バケット，CloudFront web distributionを定義し，先に作成したWAFv2 ACLと関連付けていくのですが，
ここでの問題は，US East (N. Virginia) RegionのParameter Storeに格納されているWebACL ARNを参照しなければならない点です．
cross-regionの参照は通常の方法では不可能なため，[こちら](https://stackoverflow.com/questions/59774627/cloudformation-cross-region-reference)を参考にカスタムリソースを使った[参照クラス](./lib/ssm_parameter_reader.ts)を実装して解決しました．

## ハマったポイント

- WAFv2 リソースのリージョン制限

   WAFv2のWebACLをCloudFrontに関連付ける場合，ACLをus-east-1に作成する必要がありました．

- CDKでの他リージョンのSSM Parameter Store参照
  
  ```ts
  const param = ssm.StringParameter.valueForStringParameter(this, `param-name`);
  ```
  の方法で参照できるのは，そのスタックの同一リージョンのParameter Storeのみです．
  cross-region参照するため，カスタムリソースを使った方法の情報にたどり着くまで苦労しました...

- CDKでのCloudFrontへのWAF WebACLの関連付け
   
   `@aws-cdl/aws-wafv2`モジュールに[CfnWebACLAssociation](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-wafv2.CfnWebACLAssociation.html)というクラスがあり，最初，これを使えばよいかと思ったのですが，こちらで関連付けできるのは，`API Gateway REST API等，CloudFront distribution以外`の場合でした．
CloudFrontと関連付ける場合は，[CloudFrontWebDistribution側](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cloudfront.CloudFrontWebDistribution.html)で，WebACLを指定する必要があります．
webACLのプロパティ名はwebACLIdとなっていますが，WAFv2を使う場合`WebACLのARNを指定`する点にも注意してください．

   ```ts
    this.distribution = new cloudfront.CloudFrontWebDistribution(
      this, "website-distribution",
      {
        webACLId: webAclArn,
        // 以下省略
      }
    );
   ```

## ACLの中身

本サンプルでは，IPアドレスベースの制限としています．アクセスを許可するIPアドレスは，
cdk.context.jsonに記述して，CDK context valueとしてインポートしています．
サンプル公開用では，ダミーのIPアドレスを記述しているので，検証したいIPアドレスに書き換えてください．

```json
{
  "allowed_ips": [
    "10.0.0.0/16"
  ]
}
```

## Reference

本スタック実装にあたり参考にさせていただいたサイトを，トピック別に掲載しました．
### WAFを使ったCloudFrontへのアクセス制限

- [CloudFront + S3 での IP アドレスベースのアクセス制限設定をする](https://ceblog.mediba.jp/post/156084994102/cloudfront-s3-%E3%81%A7%E3%81%AE-ip-%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E3%83%99%E3%83%BC%E3%82%B9%E3%81%AE%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E5%88%B6%E9%99%90%E8%A8%AD%E5%AE%9A%E3%82%92%E3%81%99%E3%82%8B)

### CDKを使ったWAFリソースのdeploy

- [AWS CDKでWAFv2を構築しIPアドレス制限を試してみた](https://dev.classmethod.jp/articles/aws-cdk-wafv2-block-ip-example/)

- [AWS WAFv2をCDKで構築してみた](https://dev.classmethod.jp/articles/aws-cdk-create-wafv2/)

- [TerraformでCloudFrontにACL(AWS WAF)を定義するときのエラー対処方法](https://qiita.com/yuu999/items/e5e233e02be0ed1d2365)

### AWS WAFの解説

- [AWS WAFを完全に理解する - WAFの基礎からv2の変更点まで -](https://dev.classmethod.jp/articles/fully-understood-aws-waf-v2/)

### WAF WebACL for CloudFrontの作成リージョンに関する注意

- [【小ネタ】AWS WAF v2 の WebACL （CloudFront用）を東京リージョンから CloudFormation で作成しようとしたら怒られた](https://dev.classmethod.jp/articles/cloudformation-webacl-cloudfront-error/)

- [AWS WAF v1 と v2 それぞれで WebACL を CloudFormation で作成したときにハマった話](https://michimani.net/post/aws-create-web-acl-at-waf-v1-v2/)

### AWS CDKで他のリージョンのssm parameter storeを参照する方法

- [CloudFormation Cross-Region Reference | stack overflow](https://stackoverflow.com/questions/59774627/cloudformation-cross-region-reference)