AWS WAFのカスタムレスポンスでRESTもGraphQLも対応できるAPIのメンテナンスモードを実現する

2021年03月にリリースされたAWS WAF v2のカスタムレスポンス機能を使って、APIから「メンテナンス中」の旨を返すようなメンテナンスモードを実現してみました。


AWS WAF v2のカスタムレスポンス機能とは

  • WAFでリクエストをブロックするように設定した際に、httpステータスコードやレスポンスヘッダー・ボディを自由に定義して返せる機能

docs.aws.amazon.com


前提

APIのメンテナンスモードとは

今回実現したい「APIのメンテナンスモード」というのは、Webやアプリなどのメンテナンス中に「メンテナンス中です」のようなページを表示するために、APIからのレスポンスを変更するためのものとして書いています。

あくまでAPI側での実現方法なので、フロント側の話は今回は触れません。


また今回はAPI側をスコープにしていますが、フロント側だけでメンテナンスページを表示したい場合は、CloudFrontのカスタムエラーレスポンスとWAFを組み合わせることで実現可能です。

go-to-k.hatenablog.com

CloudFrontのカスタムエラーレスポンスを使わず、WAFのカスタムレスポンスのみでもメンテナンスページを実現することができます。

go-to-k.hatenablog.com


対象リソース

今回はAWS WAFを使用するため、APIAPI GatewayやAppsync、ELBなど、WAFがアタッチできるものに限ります。


特徴

RESTにもGraphQLにも対応できる

  • RESTのメンテナンス時の特徴
    • メンテナンスの際のステータスコードは503を使うことが多い
    • メンテナンス用のヘッダーを定義して使うことも

→WAFのカスタムレスポンス機能で、ステータスコードを503にし、カスタムヘッダーを定義して実現する

ステータスコードだけで実現することも可能ですが、503だけどメンテナンスモード時ではないときもメンテナンスモードとして扱ってしまうので、ちゃんとやるのであればヘッダーを使用した方が確実です。その場合、ステータスコードはハンドリングしなくて良いケースもあります。


  • GraphQLのメンテナンス時の特徴
    • GraphQLではエラー時もステータスコードが200のままで、ボディの「errors」というキーでhttpエラーを表現することも多い
    • なのでメンテナンス時も同じく、ステータスコード200で、レスポンスボディの「errors」で表現する(※他の方法も可能です)

→WAFのカスタムレスポンス機能で、ステータスコードは200のまま、レスポンスボディを定義して実現する

APIソースコード変更の必要がない

  • ソースコード側でレスポンスの設定や定義をするのではなくあくまでWAFによるものなので、ソースコードを一切変更せずに実現が可能です。


方法

AWS WAFのWebACLを作成し、APIリソースにアタッチすることでメンテナンスモードを実現します。

また、CloudFormation(ymlテンプレートファイル)を使用してみます。

GitHubにもあります。

github.com


REST用

ステータスコードを503にし、カスタムヘッダーを定義

  WAFWebACL:
    Type: AWS::WAFv2::WebACL
    Properties:
      Name: "Maintenance-WebACL"
      Scope: "REGIONAL"
      VisibilityConfig:
        CloudWatchMetricsEnabled: true
        MetricName: !Ref WAFWebACLMetricName
        SampledRequestsEnabled: true
      CustomResponseBodies: 
        CustomResponseBodyKeyForRest:
          ContentType: APPLICATION_JSON
          Content: '{"error": {"errorType": "MaintenanceMode", "message": "Unable to access during the maintenance."}}'
      DefaultAction:
        Block: 
          CustomResponse:
            ResponseCode: 503
            CustomResponseBodyKey: CustomResponseBodyKeyForRest
            ResponseHeaders:
              - Name: custom-error-type
                Value: "MaintenanceMode"


  • Scope
    • アタッチするリソース(API Gateway, Appsync, ELB)はリージョナルサービスのため、ScopeはREGIONALにする
Scope: "REGIONAL"


  • VisibilityConfig
    • 監視のための設定など
    • 今回はあまり触れません
      VisibilityConfig:
        CloudWatchMetricsEnabled: true
        MetricName: !Ref WAFWebACLMetricName
        SampledRequestsEnabled: true


  • CustomResponseBodies
    • カスタムレスポンスのボディの詳細をここで定義できる
      • ※RESTではボディは使わない予定でしたが、一応定義しておいてます。
    • CustomResponseBodyKeyForRestの名前は自由に決められ、このキー名をCustomResponse:で呼び出す
    • JSONで返したいため、ContentType: APPLICATION_JSONにする
    • ボディ内容
      • error
        • "errorType": "MaintenanceMode"
        • "message": "Unable to access during the maintenance."
      CustomResponseBodies: 
        CustomResponseBodyKeyForRest:
          ContentType: APPLICATION_JSON
          Content: '{"error": {"errorType": "MaintenanceMode", "message": "Unable to access during the maintenance."}}'


  • CustomResponse
    • カスタムレスポンス本体の設定を書く
      • ResponseCodeを503にする
      • CustomResponseBodyKeyには先程のCustomResponseBodyKeyForRest
      • ResponseHeadersには カスタムヘッダーのNameとValueを自由に決める
        • Name: custom-error-type
        • Value: MaintenanceMode
      DefaultAction:
        Block: 
          CustomResponse:
            ResponseCode: 503
            CustomResponseBodyKey: CustomResponseBodyKeyForRest
            ResponseHeaders:
              - Name: custom-error-type
                Value: MaintenanceMode


GraphQL用

ステータスコードは200のまま、レスポンスボディを定義

  WAFWebACL:
    Type: AWS::WAFv2::WebACL
    Properties:
      Name: "Maintenance-WebACL"
      Scope: "REGIONAL"
      VisibilityConfig:
        CloudWatchMetricsEnabled: true
        MetricName: !Ref WAFWebACLMetricName
        SampledRequestsEnabled: true
      CustomResponseBodies: 
        CustomResponseBodyKeyForGraphql:
          ContentType: APPLICATION_JSON
          Content: '{"errors": [{"errorType": "MaintenanceMode", "message": "Unable to access during the maintenance."}]}'
      DefaultAction:
        Block: 
          CustomResponse:
            ResponseCode: 200
            CustomResponseBodyKey: CustomResponseBodyKeyForGraphql


  • CustomResponseBodies
    • "errors"キーの中に配列を持たせ、その中でエラー内容(メンテナンス内容)を定義
      • errors配列
        • "errorType": "MaintenanceMode"
        • "message": "Unable to access during the maintenance."
      CustomResponseBodies: 
        CustomResponseBodyKeyForGraphql:
          ContentType: APPLICATION_JSON
          Content: '{"errors": [{"errorType": "MaintenanceMode", "message": "Unable to access during the maintenance."}]}'


  • CustomResponse
    • ResponseCodeを200にする
    • 先程定義したCustomResponseBodyKeyForGraphqlを呼ぶ
    • RESTの方で定義したResponseHeadersは記述しない(しても良い)
      DefaultAction:
        Block: 
          CustomResponse:
            ResponseCode: 200
            CustomResponseBodyKey: CustomResponseBodyKeyForGraphql


WAF作成後

上記yamlをもとにCloudFormationでデプロイし、APIリソースにWAFをアタッチすると、APIへのアクセスがブロックされて定義したレスポンスが返るようになります。

※デプロイ方法やアタッチ方法は省略

$ curl -i ${url}   #<- urlにはAPIのエンドポイントを入れる
HTTP/2 503 
content-type: application/json
...(省略)
x-amzn-errortype: ForbiddenException
custom-error-type: MaintenanceMode
...(省略)

{"error": {"errorType": "MaintenanceMode", "message": "Unable to access during the maintenance."}}


あとはフロント側のjavascriptでレスポンスをハンドリングしてメンテナンスページに遷移させたり、CloudFrontのカスタムエラーレスポンス機能を使用したりすれば、メンテナンスページの表示までできるようになります。