AWS WAF v2でCloudFrontのIPアドレス制限を作成する

## この記事の目的は？

**CloudFront + AWS WAF**は非常に汎用性のある構成であるにも関わらずCloudFormation自動作成の記事が少なかったので、実装方法を記載します


### どんなものを実装できるの？

特定のIPのみアクセス可能なCloudFrontを**yaml**から作成できます

### 構成

![composition.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2171325/c19c9cae-d538-11b1-ee1a-67beae77f093.png)


### 注意することその1

CloudFront+WAFの構成を作る上で気をつけなければいけないのが作成順序です

CloudFrontを作ってからWAFを作って、関連付けるのではなく

**WAFを作ってからCloudFrontを作って、関連付ける**のが正しいです

理由：CloudFrontディストリビューションはARNでWAFを参照できますが、WAFはCloudFrontを参照する機能が無いからです


### 注意することその2

WAFをCloudFormationで作成する場合、CloudFormationを実行するリージョンは**米国東部 (バージニア北部)us-east-1**で実行して下さい

その他リージョンで実行しても失敗してしまいます

参考元：https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-webacl.html

> For `CLOUDFRONT`, you must create your WAFv2 resources in the US East (N. Virginia) Region, `us-east-1`.

## 作ってみた

#### 実現したいことはこの３つ

1. アクセスを許可させたいIP群のリストを作成する
2. そのリストにあるIPのみアクセス可能なWAFを作成する
3. 作成したWAFをCloudFront用コードから参照して関連付けられるよう、WAFのARNの値を出力できるようにしておく

さっそく、CloudFormationのコードをyamlで作成します



#### CloudFormationをyaml言語で作成

```yaml:wafv2.py
AWSTemplateFormatVersion: '2010-09-09'
Description: WAF by Cloud Front
Parameters:
Resources:
  WebACL:                          # WAF本体はこの  WebACL で作成します
    Type: AWS::WAFv2::WebACL
    Properties:
      Name: WebACL_test
      Scope: CLOUDFRONT            # CloudFront用のWAFなのでScopeは CLOUDFRONT にしましょう
      DefaultAction:
        Block: {}
      VisibilityConfig:
        SampledRequestsEnabled: true
        CloudWatchMetricsEnabled: true
        MetricName: webaclcloudfront
      Rules:                       # WAFのアクセス制御の具体的な方法はこの Rules で設定します
        -
          Name: rules-proxy-test
          Priority: 0
          Action:
            Allow: {}
          Statement:
            IPSetReferenceStatement:
              Arn: !GetAtt WAFIPSet.Arn  # ここでは後述の WAFIPSet で作成したIP情報のARNを参照するようにしています
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: rules-proxy-test
   
  WAFIPSet:                              # アクセスを許可したいIPはこの WAFIPSet で設定します
    Type: AWS::WAFv2::IPSet
    Properties:
      Name: wafipset-test
      Scope: CLOUDFRONT                  # WebACLと同じくここもCLOUDFRONTと指定します
      IPAddressVersion: IPV4
      Addresses:                         # 許可させたいIPをこの Addresses に記載します
          - xxx.xxx.xxx.xxx/32
          - yyy.yyy.yyy.yyy/24
          - zzz.zzz.zzz.zzz/28
Outputs:
  CloudFrontWAF:
    Value: !GetAtt WebACL.Arn      # WAFのARNを CloudFrontWAF という論理名で出力して、CloudFrontから参照できるようにします
```

##  CloudFormationで読み込んだらリソースが作成されました
![0.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2171325/6a15bb4a-a01d-be58-6475-4a646cb8fdd5.png)


#### 1および2を実現できました
1. アクセスさせたいIPのリストを作成する **(WAFIPSet)**
2. そのIPリストを元にIPでアクセス制御できるWAFを作る**(WebACL)**

![1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2171325/9d1f0e14-4c42-5651-bf36-c55808c19bc9.png)


#### 3を実現できました

3. 作成したWAFをCloudFront用コードから参照して関連付けられるよう、WAFのARNの値を出力できるようにしておく**(CloudFrontWAF)**

![2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2171325/394d340d-295a-cc8b-b983-9cb662a35e97.png)




## 実装までにつまずいたところ
最初はStatementの設定を以下のようにしていました

```
          Statement:
            IPSetReferenceStatement: 
              !Ref WAFIPSet
```

CloudFormationにインポートしたら、以下のようなモデル検証エラーが発生

#### 原文

```
Resource handler returned message: "Model validation failed (
  #/Rules/0/Priority: expected type: Number, found: String 
  #/Rules/0/Statement/IPSetReferenceStatement: expected type: JSONObject, found: String 
  #/Rules/0/VisibilityConfig/SampledRequestsEnabled: expected type: Boolean, found: String 
  #/Rules/0/VisibilityConfig/CloudWatchMetricsEnabled: expected type: Boolean, found: String 
  #/VisibilityConfig/SampledRequestsEnabled: expected type: Boolean, found: String 
  #/VisibilityConfig/CloudWatchMetricsEnabled: expected type: Boolean, found: String)" (RequestToken: f1cbb0f3-5861-59ab-ffbc-36c56447b5b0, HandlerErrorCode: InvalidRequest)

```

#### 和訳

```
リソースハンドラーが返すメッセージ： "モデルの検証に失敗しました（
＃/ Rules / 0 /優先度：期待されるタイプ：数値、検出されました：文字列
＃/ Rules / 0 / Statement / IPSetReferenceStatement：期待されるタイプ：JSONObject、検出されました：文字列
＃/ Rules / 0 / VisibilityConfig / SampledRequestsEnabled：期待されるタイプ：ブール値、検出：文字列
＃/ Rules / 0 / VisibilityConfig / CloudWatchMetricsEnabled：期待されるタイプ：ブール値、検出：文字列
＃/ VisibilityConfig / SampledRequestsEnabled：期待されるタイプ：ブール値、検出：文字列
＃/ VisibilityConfig / CloudWatchMetricsEnabled：予期されるタイプ：ブール値、検出：文字列） 
```


### エラーの原因は戻り値の指定の仕方

ちゃんとAWS公式リファレンスに書かれていました 

参考元：https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-webacl.html




>参照
>Refリソース名、物理ID、および範囲を含むリソースのため、次のようにフォーマットされました：name|id|scope 
>例：my-webacl-name|xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx|REGIONAL
>Fn :: GetAtt
>Arn
>Web ACLのAmazonリソース名（ARN）


戻りの値は**!Ref**ではなく**!GetAtt**で指定する必要があるようです 


コードを以下のように書き直して再度CloudFormationで実行したところ、成功しました！

```
         Statement:
            IPSetReferenceStatement:
              Arn: !GetAtt WAFIPSet.Arn
```


##  終わりに

CloudFront＋WAFというありふれた構成であるにも関わらず、その構成をCloudFormationで実現する情報がウェブにはほぼ皆無だったため、非常に難儀しました 

なので今後似たような状況で苦しんでいる方々のために、なるべく最小構成でコードを作っております 

今回の調査のため色々なblogを巡回しましたが、答えは最初からAWS公式リファレンスにすべて書かれており、改めてAWSドキュメントの完成度の高さに感動しました 

## 補足

参考になったページ

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-webacl.html

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-ipset.html