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


※2020/12/6追記 DependsOnを使ってCloudFromationテンプレートを書き換えました。

はじめに

こんにちは!
本記事は先日公開した【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る!第1回(全3回)の続きとなります。

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

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

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

今回作るもの(再掲)

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

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::WAFAWS::WAFRegional)。

GlobalなWAFとRegionalなWAF

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

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

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

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

WAFの構成要素

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

  • IPセット
  • WebACL
  • Association

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

IPセット

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

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

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セットのリソース名を指定しています。

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 ルールの詳細をリスト形式で記載します。

Action→ルールに一致した場合の挙動を指定

Priority→ルールの優先度。最小値は0で、小さい順から優先的にルール判定が行われる

Statement→ルールの具体的な内容を指定します。今回はIP制限のため、IPSetReferenceStatementを指定し、先ほどのIPセットのArnを指定しています
Scope CLOUDFRONTもしくはREGIONALを指定します。今回はリージョンごとのサービスであるAPI Gatewayに適用するため、上記コード例ではREGIONALを指定します
VisibilityConfig CloudWatchメトリクスを送信するかどうか、メトリクス名、Webリクエストのサンプリングを行うかどうか指定します。今回はすべてfalseにしていますが、必要であればtrueに変えてください。

Association

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

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で出力した値を読み込むことができます

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の実行方法はこちらを参照してください。

おわりに

今回は先日公開した【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る!第1回(全3回)で作成したAPI GatewayにAWS WAFを適用する方法を解説しました。
次回はバックエンドのロジックを作りこんでいきたいと思います。