CloudFormationからのWAFの設定（CloudFront/APIGateway）

# はじめに
AWSでサーバレスなアーキテクチャを採用していて、 各エンドポイント（CloudFront や API Gateway）にIP制限をかけたい場合 [AWS WAF](https://aws.amazon.com/jp/waf/) を用いることが多いかと思います。
この記事では、CloudFormation から各リソースと WAF のリソースを生成する際にハマりポイントが多かったのでまとめてみました。

# AWS::WAF と AWS::WAFRegional
まずハマりやすいのが、この２つのリソースタイプがあること。
設定できるリソースやパラメータにあまり違いはないのですが、使い分けとしては、リージョンを持つサービスに紐づけるか、リージョンを持たないサービス（CloudFront）に紐づけるか、でどちらを選択するか決まります。

- **AWS::WAFRegional　→　API Gateway, ALB, etc...**
- **AWS::WAF　→　CloudFront**

と考えておけばOKだと思います。（これを逆にすると権限のエラーで失敗します。）

# AWS WAF と AWS WAF Classic
この２つの違いも初見の人は注意が必要です。
AWS WAF は2019年11月にアップデートされていて、コンソールからサービスを利用する場合、はじめに新バージョンのサービスが表示されます。
旧バージョンの AWS WAF を利用する場合、左部メニューにこっそりある `Switch to AWS WAF Classic` をクリックすると切り替わります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/356162/9982907f-d144-e89c-9bec-b4d481192674.png)

ここでハマりどころなのが、 CloudFormation のリソースタイプの `AWS::WAF` と `AWS::WAFRegional` は、どちらも旧バージョン（AWS WAF Classic）のリソースを作成するものであるということです。
AWSサポートに確認したわけではないので確かではありませんが、現時点（2019年12月時点）では、CloudFormation から新バージョンの AWS WAF のリソースを生成することはできなそうです。（今後対応されそう）

# template ファイル
今回、実際に CloudFront と API Gateway それぞれに紐づいた AWS WAF (Classic) を CloudFormation から生成しました。
その際、template の記述に関しても注意すべき点があったので、 コメントを追記した `template.yaml` を載せておきます。

## 共通
IP制限を行う際に、 `WebACL` + `Rule` + `IPSet` のリソースをセットで使用するのは、 `AWS::WAF` も `AWS::WAFRegional` も同様。

## AWS::CloudFront + AWS::WAF パターン
`AWS::WAF` の場合は、 CloudFront のリソース側から紐付けを行います。

```yaml:template.yaml(CloudFront)
Parameters:
  AppName:
    Type: String
  AllowedIP:
    Type: String

Resources:
  MyDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      # 〜その他のプロパティは省略〜
      DistributionConfig:
        WebACLId: !Ref MyWebACL #紐付けはここで行う

  # AWS::WAF の場合、リソースの紐付けに WebACLAssociation は使用しない

  MyWebACL:
    Type: AWS::WAF::WebACL
    Properties:
      Name: !Sub ${AppName}WebACL #ハイフンやアンスコは使用不可
      DefaultAction:
        Type: BLOCK
      MetricName: !Sub ${AppName}WebACL
      Rules:
        -
          Action:
            Type: ALLOW
          Priority: 1
          RuleId: !Ref MyRule

  MyRule:
    Type: AWS::WAF::Rule
    Properties:
      Name: !Sub ${AppName}AllowedRule #ハイフンやアンスコは使用不可
      MetricName: !Sub ${AppName}AllowedRule
      Predicates:
        -
          DataId: !Ref MyIPSet
          Negated: false
          Type: IPMatch

  MyIPSet:
    Type: AWS::WAF::IPSet
    Properties:
      Name: !Sub ${AppName}AllowedIPSet #ハイフンやアンスコは使用不可
      IPSetDescriptors:
        -
          Type: IPV4
          Value: !Sub ${AllowedIP}/32 #IPは末尾に「/32」を付与

```
## AWS::Serverless::Api + AWS::WAFRegional パターン
`AWS::WAFRegional` の場合、 `WebACLAssociation` というリソースを利用して紐付けを行います。
[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-wafregional-webaclassociation.html)では、ロードバランサーとの紐付けの例しかありませんが、API Gateway と紐付けを行う場合は以下のように記述します。

```yaml:template.yaml(+APIGateway)
Parameters:
  AppName:
    Type: String
  Stage:
    Type: String
  AllowedIP:
    Type: String

Resources:
  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      # 〜その他のプロパティは省略〜
      StageName: !Ref Stage
    
  MyWebACLAssociation: #紐付けはこのリソースで行う
    Type: "AWS::WAFRegional::WebACLAssociation"
    Properties:
      ResourceArn: !Sub arn:aws:apigateway:${AWS::Region}::/restapis/${MyApi}/stages/${Stage}
      # ResourceArn は !Ref MyApi ではないので注意
      WebACLId: !Ref MyWebACL

  MyWebACL:
    Type: AWS::WAFRegional::WebACL
    Properties:
      Name: !Sub ${AppName}APIWebACL #ハイフンやアンスコは使用不可
      DefaultAction:
        Type: BLOCK
      MetricName: !Sub ${AppName}APIWebACL
      Rules:
        -
          Action:
            Type: ALLOW
          Priority: 1
          RuleId: !Ref MyRule

  MyRule:
    Type: AWS::WAFRegional::Rule
    Properties:
      Name: !Sub ${AppName}APIAllowedRule #ハイフンやアンスコは使用不可
      MetricName: !Sub ${AppName}APIAllowedRule
      Predicates:
        -
          DataId: !Ref MyIPSet
          Negated: false
          Type: IPMatch

  MyIPSet:
    Type: AWS::WAFRegional::IPSet
    Properties:
      Name: !Sub ${AppName}APIAllowedIPSet #ハイフンやアンスコは使用不可
      IPSetDescriptors:
        -
          Type: IPV4
          Value: !Sub ${AllowedIP}/32 #IPは末尾に「/32」を付与
```

# 最後に
今回は、サーバレスアーキテクチャでの CloudFormation を用いた WAF の設定のハマりポイントを解説しました。
AWSの公式ドキュメントは本当にわかりづらいので、同じようなことでハマっている人は参考にしていただければ幸いです。