🔧

AWS 環境でメンテナンスモードを実現する

2024/07/10に公開

目的

  • メンテナンスモード中は全てのリクエストをメンテナンスページへ誘導したい
  • メンテナンスモードと非メンテナンスモードの切り替えを簡単にしたい
  • プロダクトの関係者が動作確認することができる

さっそく結論ですが、WAF でメンテナンスモードを実現することにした。理由は以下の通り。

  • ルールの切り替えだけで済むため非常にシンプル
  • ルールを工夫しておけばプロダクトの関係者が動作確認するため穴を開けておくことができる

ざっくりとしたインフラ構成図は以下の通りで、前段にいる WAF でブロックしメンテナンスページを返すイメージ。
イメージ

実装にあたり以下のページを参考にさせていただいた。
https://zenn.dev/daicho/scraps/6772790f2881e6

実装内容

インフラリソースの管理をCDK にて行っていた関係上、メンテナンスモード用の WAF のルールも CDK 管理をするべきか検討したが、メンテナンスモードと非メンテナンスモードの切り替えが困難かつ手順が複雑化するため CDK 管理は断念した。 (CDK で管理するべき内容ではないのもある)
シンプルに AWSCLI を利用してスクリプトにて実現した。

利用する wafv2 コマンド は以下。

  • list-web-acls
  • get-web-acl
  • update-web-acl

メンテナンスモードの開始

メンテナンスモードの開始方法は以下の通り。

  1. wafv2 list-web-aclsコマンドを利用して、操作対象の WebACL の Id と Name を取得する
  2. 1で取得した WebACL の Id と Name でwafv2 get-web-aclコマンドを利用して、WebACL の現在の状態を取得する
  3. 2で取得した WebACL から LockToken を取得する
  4. 2で取得した WebACL から現在のルールを取得する
  5. メンテナンスモード用のルールを取得する
  6. すでにメンテナンスモードが開始されている場合は処理を終了する
  7. 現在のルールにメンテナンスモード用のルールを追加して新しいルールを作成する
  8. 3で作成した LockToken と7で作成した新しいルールで、wafv2 update-web-aclコマンドを利用してルールを更新する

メンテナンスモードのコード例 (一部抜粋)

メンテナンスモード用ルール

Priority は現在のルールの最大より大きなものを設定し、最後に評価されるように調整する。
この例では、CustomResponseBodyKey が JSONMaintenanceModeResponse のカスタムレスポンスがレスポンスとして返却される設定。

maintenance_mode_rule.json
{
  "Name": "MaintenanceMode",
  "Priority": 100,
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "MaintenanceMode"
  },
  "Statement": {
    "RegexMatchStatement": {
      "FieldToMatch": {
        "UriPath": {}
      },
      "TextTransformations": [
        {
          "Type": "NONE",
          "Priority": 0
        }
      ],
      "RegexString": "/.*"
    }
  },
  "Action": {
    "Block": {
      "CustomResponse": {
        "ResponseHeaders": [
        ],
        "ResponseCode": 503,
        "CustomResponseBodyKey": "JSONMaintenanceModeResponse"
      }
    }
  }
}

カスタムレスポンス

上記ルールの CustomResponseBodyKey に設定したカスタムレスポンスの実際の内容。

custom_response_bodies.json
{
  "JSONMaintenanceModeResponse": {
    "ContentType": "APPLICATION_JSON",
    "Content": "{\"type\": \"xxxx\",\"message\": \"Service Temporarily Unavailable\"}"
  }
}

スクリプトの抜粋

メンテナンスモード開始用のスクリプト。

start_maintenance_waf.sh
#!/usr/bin/env bash

... 省略 ...

get_web_acl_details() {
  # 1. 操作対象の WebACL の Id と Name を取得する
  local web_acl_details=$(aws wafv2 list-web-acls --scope $SCOPE --region $REGION)
  local rule=$(echo $web_acl_details | jq ".WebACLs[] | select(.Name|test(\".*$APP\"))")
  WEB_ACL_ID=$(echo $rule | jq -r '.Id')
  WEB_ACL_NAME=$(echo $rule | jq -r '.Name')

  if [ -z "$WEB_ACL_ID" ] || [ -z "$WEB_ACL_NAME" ]; then
    echo "Error: WEB_ACL_ID or WEB_ACL_NAME not found" >&2
    exit 1
  fi
}

create_update_rule() {
  # 2. IdとNameを利用して WebACL の現在の状態を取得する
  local current_web_acl=$(aws wafv2 get-web-acl --id $WEB_ACL_ID --name $WEB_ACL_NAME --scope $SCOPE --region $REGION)
  # 3. LockToken を取得する
  LOCK_TOKEN=$(echo $current_web_acl | jq -r '.LockToken')
  # 4. 現在のルールを取得する
  local current_rules=$(echo $current_web_acl | jq '.WebACL.Rules')
  # 5. メンテナンスモード用のルールを取得する
  local new_rule=$(jq maintenance_mode_rule.json)
  local new_rule_name=$(echo $new_rule | jq -r '.Name')

  # 6. すでにメンテナンスモードが開始されている場合は処理を終了する
  if echo $current_rules | jq -e ".[] | select(.Name == \"$new_rule_name\")" > /dev/null; then
    echo "MaintenanceMode rule already exists."
    exit 1
  fi

  # 7. 現在のルールにメンテナンスモード用のルールを追加して新しいルールを作成する
  UPDATED_RULES=$(echo $current_rules | jq ". += [$new_rule]")
}

... 省略 ...

get_web_acl_details

create_update_rule

echo $UPDATED_RULES | jq
echo -n "↑↑↑ Ready to update?(y/n) :"
read INPUT

if [[ "$INPUT" == "y" ]]; then
  # 8. LockToken と新しいルールで更新する
  aws wafv2 update-web-acl \
    --id $WEB_ACL_ID \
    --scope $SCOPE \
    --region $REGION \
    --name $WEB_ACL_NAME \
    --default-action Block={} \
    --visibility-config SampledRequestsEnabled=false,CloudWatchMetricsEnabled=false,MetricName="$WEB_ACL_NAME" \
    --custom-response-bodies file://custom_response_bodies.json \
    --rules "$UPDATED_RULES" \
    --lock-token $LOCK_TOKEN
    echo "Maintenance mode rule added."
else
  echo "Update cancelled."
fi

手順

  1. スクリプトを実行する
    bash start_maintenance_waf.sh
    
  2. AWS マネージメントコンソールでWAFのルールにメンテナンスモード用のルールが追加されていることを確認する
  3. なんらかの API を実行し、ステータスコードが503かつ設定したJSONが返却されることを確認する

メンテナンスモードの終了

手順としては以下の通り。

  1. wafv2 list-web-aclsコマンドを利用して、操作対象の WebACL の Id と Name を取得する
  2. 1で取得した WebACL の Id と Name でwafv2 get-web-aclコマンドを利用して、WebACL の現在の状態を取得する
  3. 2で取得した WebACL から LockToken を取得する
  4. 2で取得した WebACL から現在のルールを取得する
  5. メンテナンスモード用のルールを取得する
  6. メンテナンスモードが開始されていない場合は処理を終了する
  7. 4で取得した現在のルールからメンテナンスモード用のルールを削除して新しいルールを作成する
  8. 3で作成した LockToken と7で作成した新しいルールで、wafv2 update-web-aclコマンドを利用してルールを更新する

スクリプトの抜粋

メンテナンスモード終了用のスクリプト。

end_maintenance_waf.sh
#!/usr/bin/env bash

... 省略 ...

get_web_acl_details() {
  # 1. 操作対象の WebACL の Id と Name を取得する
  local web_acl_details=$(aws wafv2 list-web-acls --scope $SCOPE --region $REGION)
  local rule=$(echo $web_acl_details | jq ".WebACLs[] | select(.Name|test(\".*$APP\"))")
  WEB_ACL_ID=$(echo $rule | jq -r '.Id')
  WEB_ACL_NAME=$(echo $rule | jq -r '.Name')

  if [ -z "$WEB_ACL_ID" ] || [ -z "$WEB_ACL_NAME" ]; then
    echo "Error: WEB_ACL_ID or WEB_ACL_NAME not found" >&2
    exit 1
  fi
}

create_update_rule() {
  # 2. IdとNameを利用して WebACL の現在の状態を取得する
  local current_web_acl=$(aws wafv2 get-web-acl --id $WEB_ACL_ID --name $WEB_ACL_NAME --scope $SCOPE --region $REGION)
  # 3. LockToken を取得する
  LOCK_TOKEN=$(echo $current_web_acl | jq -r '.LockToken')
  # 4. 現在のルールを取得する
  local current_rules=$(echo $current_web_acl | jq '.WebACL.Rules')
  ## 5. メンテナンスモード用のルールを取得する
  local new_rule_name=$(cat maintenance_mode_rule.template.json | jq -r '.Name')

  # 6. メンテナンスモードが開始されていない場合は処理を終了する
  local rule_exists=$(echo $current_rules | jq -e ".[] | select(.Name == \"$new_rule_name\")")
  if [ ! "$rule_exists" ]; then
    echo "No maintenance mode rule found."
    exit 1
  fi
  # 7. 現在のルールからメンテナンスモード用のルールを削除して新しいルールを作成する
  UPDATED_RULES=$(echo $current_rules | jq "del(.[] | select(.Name == \"$new_rule_name\"))")
}

... 省略 ...

get_web_acl_details

create_update_rule

echo $UPDATED_RULES | jq
echo -n "↑↑↑ Ready to update?(y/n) :"
read INPUT

if [[ "$INPUT" == "y" ]]; then
  # 8. LockToken と 新しいルールで更新する
  aws wafv2 update-web-acl \
    --id $WEB_ACL_ID \
    --scope $SCOPE \
    --region $REGION \
    --name $WEB_ACL_NAME \
    --default-action Allow={} \
    --visibility-config SampledRequestsEnabled=false,CloudWatchMetricsEnabled=false,MetricName="$WEB_ACL_NAME" \
    --rules "$UPDATED_RULES" \
    --lock-token $LOCK_TOKEN
  echo "Maintenance mode rule deleted."
else
  echo "Update cancelled."
fi

手順

  1. スクリプトを実行する
    bash end_maintenance_waf.sh
    
  2. AWS マネージメントコンソールでWAFのルールにメンテナンスモード用のルールが存在しないことを確認する
  3. なんらかの API を実行し、正常レスポンスが返却されることを確認する

Discussion