【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る！第2回 WAF適用編(全3回)

※2020/12/6追記 DependsOnを使ってCloudFromationテンプレートを書き換えました。
# はじめに
こんにちは！
本記事は先日公開した[【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る！第1回(全3回)](https://qiita.com/nekoze-climber/items/30ca7da8d86629d1e50c)の続きとなります。

前回はSwaggerで記載したAPI定義を取り込んだAPI Gatewayと簡単なメッセージを返すだけのLambda関数を実装し、Serverless Frameworkを使ってAWSへデプロイするところまで行いました。

第2回ではAWS WAFを適用し、特定のIPからしかAPI Gatewayにアクセスできないようにします。これはAPI Gatewayはパブリックに公開されてしまうので、なにもアクセス制限をしないとURLさえ知っていればアクセスできてしまうためです。IP制限をかけることで安心して開発ができるようになります。

前回、第三回の記事はこちら

* [【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る！第1回(全3回)](https://qiita.com/nekoze-climber/items/30ca7da8d86629d1e50c)
* [【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る！第3回 Dynamo編(全3回)](https://qiita.com/nekoze-climber/items/03f201d4adfe4b754059)

# 今回作るもの(再掲)
今回は以下のアーキテクト図のようなWeb APIバックエンドを作っていきます。
API GatewayでクライアントからのAPIリクエストを受信し、該当するLambda関数を呼び出し、必要に応じてDynamoDBからのデータ読み出しおよび書き込みを行います。さらに、WAFを適用することでセキュアにします。

![slsTestAppArchitect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671605/484ad570-257d-1e01-de43-13376c137266.png)

APIとしては、ID・名前・身長・体重・年齢の情報を持つPersonモデルを登録・取得・更新・削除するAPIを作りたいと思います。

- GET dev/slsTestApp/v1/api/person/{personId}
- POST dev/slsTestApp/v1/api/person
- PUT dev/slsTestApp/v1/api/person/{personId}
- DELETE dev/slsTestApp/v1/api/person/{personId}

第1回目の記事では、具体的なAPIロジックは実装せず、簡単なメッセージを返すだけのLambda関数をバックエンドとするAPI Gatewayを作りました。第2回となる本記事ではWAFを適用していきます。

# AWS WAFについて
AWS WAFはCloudFront、API Gateway、ALBなどへのアクセス制限をかけることができるマネージドサービスです。
2019年11月にアップデートが行われており、CloudFromationのテンプレートのリソース名では```AWS::WAFv2```と表現されます(古いバージョンは```AWS::WAF```と```AWS::WAFRegional```)。

## GlobalなWAFとRegionalなWAF
AWS WAFには大きく分けて二種類あり、一つはリージョンを持たないグローバルなAWSサービスに紐づけるGlobalなWAFと、もう一つはリージョンを持つAWSサービスに紐づけるRegionalなWAFです。
簡単にいうと、

* GlobalなWAF →　CloudFront
* RegionalなWAF → API Gateway、ALBほか

という関係になります。それぞれ旧バージョンのWAFでは```AWS::WAF```と```AWS::WAFRegional```で表現されます。
新バージョンのWAFv2ではGlobalかRegionalかでリソースは分かれず、Scopeプロパティでどちらにするかを指定する方式に変わりました。

ここまで大雑把に新旧のWAFについて説明してきましたが、**本記事では新バージョンのWAFv2を使うこととします。**
以降、WAFと記載してもそれはWAFv2を指すこととします。

# WAFの構成要素

今回はIP制限を掛けるため、以下の三つが大きな構成要素となります。

* IPセット
* WebACL
* Association

各要素について、簡単な説明とテンプレート例を示します。

## IPセット
対象のIPを配列形式で指定します。
ここで作成するのはIPのリストのみです。WebACLのアクセスルールでこのIPリストにあるもののみ通信を許可するか(ホワイトリスト形式)、反対にこのIPリストにあるもののみ通信を拒否するか(ブラックリスト形式)選ぶことができます。ほとんどの場合は前者のホワイトリスト形式だと思います。

テンプレート例は以下の通りです。
IPは皆さんの自宅のIPなどを指定してください。

```yml:waf.yml(IPセット部分抜粋)
Resources:
  SlsTestAppIPSet:
    Type: AWS::WAFv2::IPSet
    Properties:
      Addresses:
        - ${self:provider.environment.ALLOWEDIP}
      Description: IP set for slsTestApp Access.
      IPAddressVersion: IPV4
      Name: SlsTestAppAPIAllowedIPSet
      Scope: REGIONAL
```

|プロパティ名|説明|
|:------|:------|
|Addresses|CIDER表記のIPアドレスを配列形式で指定します。上記コード例ではserverless.ymlの環境変数に指定したものを記載する方式にしています|
|Description|IPセットの説明を記載します(任意)|
|IPAddressVersion|IPV4またはIPV6を指定します|
|Name|IPセットの名前を指定します(任意)|
|Scope|CLOUDFRONTもしくはREGIONALを指定します。今回はリージョンごとのサービスであるAPI Gatewayに適用するため、上記コード例ではREGIONALを指定します |

## WebACL

WAFの具体的なルールを設定します。
テンプレート例は以下の通りです。IPセット構築後に作成したいので、```DependsOn```でIPセットのリソース名を指定しています。

```yml:waf.yml(WebACl部分抜粋)
Resources:
  SlsTestAppWebACL:
    Type: AWS::WAFv2::WebACL
    Properties:
      DefaultAction:
        BLOCK: {}
      Description: WebACL for slsTestApp Access.
      Name: slsTestAppAPIWebACL
      Rules:
        - Action:
            ALLOW: {}
          Priority: 0
          Name: SlsTestAppAPIAccessRule
          VisibilityConfig:
            CloudWatchMetricsEnabled: false
            MetricName: SlsTestAppRuleMetric
            SampledRequestsEnabled: false
          Statement:
            IPSetReferenceStatement:
              Arn:
                "Fn::GetAtt": [SlsTestAppIPSet, Arn]
      Scope: REGIONAL
      VisibilityConfig:
        CloudWatchMetricsEnabled: false
        MetricName: SlsTestAppWebACLMetric
        SampledRequestsEnabled: false
    DependsOn: SlsTestAppIPSet
```

|プロパティ名|説明|
|:------|:------|
|DefaultAction|WebACLのどのルールにも当てはまらない場合の動作を指定します。AllowもしくはBlockです。ルール外のリクエストは弾きたいので、上記コード例では```Block```を指定しています|
|Description|IPセットの説明を記載します(任意)|
|Name|IPセットの名前を指定します(任意)|
|Rules|ルールの詳細をリスト形式で記載します。<br><br>Action→ルールに一致した場合の挙動を指定<br><br>Priority→ルールの優先度。最小値は0で、小さい順から優先的にルール判定が行われる<br><br>Statement→ルールの具体的な内容を指定します。今回はIP制限のため、```IPSetReferenceStatement```を指定し、先ほどのIPセットのArnを指定しています|
|Scope|CLOUDFRONTもしくはREGIONALを指定します。今回はリージョンごとのサービスであるAPI Gatewayに適用するため、上記コード例ではREGIONALを指定します |
|VisibilityConfig|CloudWatchメトリクスを送信するかどうか、メトリクス名、Webリクエストのサンプリングを行うかどうか指定します。今回はすべてfalseにしていますが、必要であればtrueに変えてください。|

## Association

WebACLをどのリソースに適用するかを定義します。
テンプレート例は以下の通りです。WeACL構築後に作成したいので、```DependsOn```でWebACLのリソース名を指定しています。

```yml:waf.yml(Association部分抜粋)
Resources:
  SlsTestAppWebACLAssociation:
    Type: "AWS::WAFv2::WebACLAssociation"
    Properties:
      ResourceArn: arn:aws:apigateway:${self:provider.region}::/restapis/${cf:slsTestApp-${self:provider.stage}.Id}/stages/${self:provider.stage}
      WebACLArn:
        "Fn::GetAtt": [SlsTestAppWebACL, Arn]
    DependsOn: SlsTestAppWebACL
```

```ResourceArn```にWAF(正確にはWebACL)を適用したいリソースのArn、 ```WebACLArn```に適用するWebACLのArnを指定します。

API Gatewayは直接Arnを出力することができなさそうだったので、代わりにIDを出力するようAPIGatewayのテンプレートにOutputsを追加しそれを参照するようにしています。
ちなみにServerless Frameworkでは```${cf:{スタック名}.{Outputsのキー名}}```という書式を使うことで別スタックのOutputsで出力した値を読み込むことができます

```yml:api-gateway.yml
Resources:
  ApiGatewayRestApi:
    Type: "AWS::ApiGateway::RestApi"
    Properties:
      Body: ${file(./templates/swagger.yaml)}
…中略…
Outputs:
  Id:
    Value:
      Ref: ApiGatewayRestApi
```
# WAFのデプロイ
serverless.ymlを作成し、```sls deploy```コマンドを実行してください。serverless.ymlの中身は```resources```でテンプレートを読み込むくらいの内容になると思います。

# 動作確認
キャリアの電波に接続したスマホなどでブラウザを開きAPIのURLを打ち込んでみてください。
```{message:Forbidden}```などのメッセージが表示されていれば成功です。
APIの実行方法は[こちらを参照](https://qiita.com/nekoze-climber/items/30ca7da8d86629d1e50c#%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D)してください。

# おわりに
今回は先日公開した[【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る！第1回(全3回)](https://qiita.com/nekoze-climber/items/30ca7da8d86629d1e50c)で作成したAPI GatewayにAWS WAFを適用する方法を解説しました。
次回はバックエンドのロジックを作りこんでいきたいと思います。