13
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「Develop fun!」を体現する! Works Human IntelligenceAdvent Calendar 2024

Day 6

メンテナンスページをWAFで作成するCDKとその切り替えをbashで実現する

Posted at

はじめに

メンテナンスページのWAFで作成し、その切り替えをbashで実現しました。
※切り替えは手動のため、CodeBuild化をその後行いました。後ほど記事にしようと思います。

また、参考にも記載しておりますが、実装するにあたって以下の記事を参考にさせていただきました。

目次

実現したこと

今回のメンテナンスページは、WAFのルールグループとカスタムレスポンスを使って制御しました。

通常

通常

CloudFrontとLambdaで構成しているアーキテクチャに、WAFで通常ルールグループとメンテナンスルールグループの2つを作成し、WebACLにアタッチしています。また、デフォルトルールグループの中に、セキュリティ対策のため5分間に1000件のリクエストがきたらブロックするルールを設け、メンテナンスルールグループの方はルールを空にしています。
そのため、5分間に1000件のリクエストを送るユーザー以外はアクセス可能です。

メンテナンス時

メンテナンス時

メンテナンス中は、ルールグループは通常時と変わりませんが、メンテナンスルールグループの中にメンテナンス担当のIPアドレスのみ許可するルールを作成し、アクセスできる人を限定的にします。
そのため、メンテナンス担当のIPアドレスからのみアクセス可能となり、その他のユーザーはWAFで503が返されてアクセスできないようになります。

実装方針

実装方針としては、今後デフォルトのルールが変わるかもしれないことを考慮し、CDKでルールグループをそれぞれ作り、デフォルトのルールグループには予めBlockHighRateを入れておき、メンテナンスの方は空として作成しました。また、メンテナンスのルールグループに入れるルール(AllowIpAddress)はJSON(maintenance_mode_rule.json)で管理する形にしました。

それぞれで行っていることに関しては以下の通りです。

  • CDK
    • IPSet(中身は空)作成
    • WAFのRuleGroup(default/maintenance)作成
    • WebACL作成
  • bash
    • start
      • パラメータで渡したIPが、IPSetにすでにあるかの確認
        • あれば処理終了
      • IP Setにパラメータで渡したIPアドレスを追加
      • maintenanceGroupにIPSetのルールを追加し、update-rule-groupを実行
      • WebACLにカスタムレスポンスを追加
    • end
      • メンテナンスグループの中身を空にする
      • WebACLデータ取得して、カスタムレスポンスを削除する
      • IPSetからIPアドレスを全て削除
    • JSON(custom_response_bodies)
      • メンテナンス時に返すレスポンスの中身
    • JSON(maintenance_mode_rule)
      • メンテナンス時、メンテナンスルールグループに入れるルールの中身(AllowIpAddress)

ディレクトリ構成

ディレクトリの構成は以下の通りです。

┣ maintenance
  ┣ jsons
    ┣ custom_response_bodies.json
    ┗ maintenance_mode_rule.json
  ┣ start_maintenance.bash
  ┗ end_maintenance.bash
┗ ap-northeast-1-stack.ts

実装したCDK

処理の流れ

CDKの処理の詳細については以下の通りです。

ap-northeast-1-stack.ts

  • IPSet作成
    • 空のIPSetを作成
  • Group(default/maintenance)
    • それぞれのARNをSSMで保存
  • WebACL
    • ARNをSSMで保存

コード

'ap-northeast-1-stack.ts'
import * as cdk from "aws-cdk-lib";
import * as ssm from "aws-cdk-lib/aws-ssm";
import * as waf from "aws-cdk-lib/aws-wafv2";
import { Construct } from "constructs";

export class ApNortheast1Stack extends cdk.Stack {
  distribution: cdk.aws_cloudfront.Distribution;
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);
    
    // IP Set
    const NULL_IP_LIST:string[] = [];
    const allowIpSetForMaintenanceName = 'allow-ip-set-for-maintenance';
    const allowIpSetForMaintenance = new waf.CfnIPSet(
      this,
      "AllowIpSetForMaintenance",
      {
        name: allowIpSetForMaintenanceName,
        scope: "REGIONAL",
        ipAddressVersion: "IPV4",
        addresses: NULL_IP_LIST,
      }
    );

    // Rules
    const blockHighRateAccessRuleName = "block-high-rate-access-rule";
    const blockHighRateAccessRule: waf.CfnWebACL.RuleProperty = {
      priority: 100,
      name: blockHighRateAccessRuleName,
      action: { block: {} },
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        sampledRequestsEnabled: true,
        metricName: '${blockHighRateAccessRuleName}',
      },
      statement: {
        rateBasedStatement: {
          aggregateKeyType: "IP",
          limit: 1000, 
        },
      },
    };

    // RuleGroup
    const defaultRuleGroupName = 'default-rule-group';
    const defaultRuleGroup = new waf.CfnRuleGroup(this, "DefaultRuleGroup", {
      name: defaultRuleGroupName,
      scope: "REGIONAL",
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        sampledRequestsEnabled: true,
        metricName: defaultRuleGroupName,
      },
      capacity: 10,
      rules: [blockHighRateAccessRule]
    });
    const maintenanceRuleGroupName = 'maintenance-rule-group';
    const maintenanceRuleGroup = new waf.CfnRuleGroup(
      this,
      "MaintenanceRuleGroup",
      {
        name: maintenanceRuleGroupName,
        scope: "REGIONAL",
        visibilityConfig: {
          cloudWatchMetricsEnabled: true,
          sampledRequestsEnabled: true,
          metricName: maintenanceRuleGroupName,
        },
        capacity: 10,
        rules:[]
      }
    );

    // WebACL
    const webACLName = '${restApi.restApiName}-web-acl';
    const webACL = new waf.CfnWebACL(this, "WebAcl", {
      name: webACLName,
      scope: "REGIONAL",
      defaultAction: {
        allow: {},
      },
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        sampledRequestsEnabled: true,
        metricName: '${webACLName}',
      },
      rules: [
        {
          name: 'default-rule-group-reference',
          priority: 100,
          overrideAction: {
            none: {},
          },
          statement: {
            ruleGroupReferenceStatement: {
              arn: defaultRuleGroup.attrArn,
            },
          },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: 'default-rule-group-reference',
          },
        },
        {
          name: 'maintenance-rule-group-reference',
          priority: 200,
          overrideAction: {
            none: {},
          },
          statement: {
            ruleGroupReferenceStatement: {
              arn: maintenanceRuleGroup.attrArn,
            },
          },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: 'maintenance-rule-group-reference',
          },
        },
      ],
    });
    
    // メンテナンスに入る際、start_maintenance_wafを実行するためにarnをパラメータストアに保存
    new ssm.StringParameter(this, "AllowIpSetForMaintenanceArnParameter", {
      parameterName: '/allow-ip-set-for-maintenance-arn',
      stringValue: allowIpSetForMaintenance.attrArn,
    });
    new ssm.StringParameter(this, "MaintenanceRuleGroupArnParameter", {
      parameterName: '/maintenance-rule-group-arn',
      stringValue: maintenanceRuleGroup.attrArn,
    });
    new ssm.StringParameter(this, "WebAclArnParameter", {
      parameterName: '/web-acl-arn',
      stringValue: webACL.attrArn,
    });

  }
}

実装したbash

処理の流れ

bashの処理の詳細については以下の通りです。

start

  • パラメータ
    • $1 WhiteIPList
  • フロー
    • IPSetARN取得
      • $1のIPアドレスがすでに追加されていたら処理終了
    • IPSetにIPアドレス追加
    • メンテナンスルールグループを取得
      • すでにメンテナンスグループにルールがあったら処理終了
    • ルール作成・更新
      • SSMの保存されたメンテナンスルールARNで既に適用されているか確認
        • 適用されていたらIPセットにIPアドレスを追加して中止。されていなかったら続ける
    • default-rule-groupを削除し、maintenance-rule-groupを適用

end

  • パラメータ
    • なし
  • フロー
    • メンテナンスグループの中身を空にする
      • メンテナンスルールグループの中身が既に空の場合は処理終了
    • WebACLデータ取得して、カスタムレスポンスを削除する
    • IPSetのARNを取得して、IPSetからIPアドレスを全て削除

コード

start

'start_maintenance.bash'
#!/usr/bin/env bash
set -eux

# 必要な情報をセット
IP_WHITE_LIST=$3 # メンテナンス中に許可するIPアドレス e.g. 0.0.0.0/32,0.0.0.1/32 
SCOPE="REAGIONAL"
REGION="ap-northeast-1"

# $1のIPがすでにあるかの確認
check_ip_in_ipset() {
  # IP_WHITE_LISTを配列に分割
  IFS=',' read -ra IP_WHITE_LIST_ARRAY <<< "$IP_WHITE_LIST"
  # IPセットのARNを取得
  local ip_set_arn_param="/$allow-ip-set-for-maintenance-arn"
  IP_SET_ARN=$(aws ssm get-parameter --name "$ip_set_arn_param" --region "$REGION" --query 'Parameter.Value' --output text)
  IP_SET_NAME=$(echo "$IP_SET_ARN" | cut -d'/' -f3)
  IP_SET_ID=$(echo "$IP_SET_ARN" | cut -d'/' -f4)

  local current_ips=$(aws wafv2 get-ip-set --id "$IP_SET_ID" --scope "$SCOPE" --region "$REGION" --name "$IP_SET_NAME" --output json | jq -r '.IPSet.Addresses[]')
  
  for ip in "${IP_WHITE_LIST_ARRAY[@]}"; do
    if echo "$current_ips" | grep -q "$ip"; then
      echo "Error: IP address $ip is already present in IP set. Exiting."
      exit 0
    fi
  done
}

# IP SetにIPアドレスを追加
add_ip_to_ipset() {
  # IPセット内の現在のIPアドレスのリストを取得
  local current_ips=$(aws wafv2 get-ip-set --id "$IP_SET_ID" --scope "$SCOPE" --region "$REGION" --name "$IP_SET_NAME" --output json | jq -r '.IPSet.Addresses')
  local ip_set_lock_token=$(aws wafv2 get-ip-set --id "$IP_SET_ID" --scope "$SCOPE" --region "$REGION" --name "$IP_SET_NAME" --output json | jq -r '.LockToken')
  
  # 追加のIPアドレスがあればIPセットに追加する
  for ip in "${IP_WHITE_LIST_ARRAY[@]}"; do
      current_ips=$(echo "$current_ips" | jq --arg ip "$ip" '. + [$ip]')
  done

  aws wafv2 update-ip-set \
    --name "$IP_SET_NAME" \
    --scope "$SCOPE" \
    --region "$REGION" \
    --id "$IP_SET_ID" \
    --addresses "$current_ips" \
    --lock-token "$ip_set_lock_token"
}

# maintenanceGroupにIPSetのルールを追加し、update-rule-groupを実行
update_maintenance_rule_group(){
    local maintenance_rule_group_arn_param="/maintenance-rule-group-arn"
    local maintenance_rule_group_arn=$(aws ssm get-parameter --name "$maintenance_rule_group_arn_param" --region "$REGION" --query 'Parameter.Value' --output text)
    local maintenance_rule_group_name=$(echo "$maintenance_rule_group_arn" | cut -d'/' -f3)
    local maintenance_rule_group_id=$(echo "$maintenance_rule_group_arn" | cut -d'/' -f4)
    local rule_group=$(aws wafv2 get-rule-group --id "$maintenance_rule_group_id" --name "$maintenance_rule_group_name" --scope "$SCOPE" --region "$REGION")

    # メンテナンスルールが既に適用されているか確認
    if  echo "$rule_group" | jq -r '.RuleGroup.Rules[].Statement.IPSetReferenceStatement.ARN' | grep -q "$IP_SET_ARN"; then
        echo "Error: The system is already in maintenance mode with ${maintenance_rule_group_name} applied." >&2
        exit 1
    fi

    # ルールグループを更新する
    local maintenance_rule_group_lock_token=$(echo "$rule_group" | jq -r '.LockToken')
    local updated_rules=$(echo "$updated_rules" | jq --arg arn "$IP_SET_ARN" \
    '.Statement.IPSetReferenceStatement.ARN = $arn' \
    ./maintenance/rules/maintenance_mode_rule.json)

    aws wafv2 update-rule-group \
    --name "$maintenance_rule_group_name" \
    --scope "$SCOPE" \
    --region "$REGION" \
    --lock-token "$maintenance_rule_group_lock_token" \
    --id "$maintenance_rule_group_id" \
    --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=MaintenanceModeRuleGroupMetrics \
    --rules "$updated_rules"
}

# WebACLにカスタムレスポンスを追加
add_custom_response_to_web_acl() {
    local web_acl_arn=$(aws ssm get-parameter --name "$web_acl_arn_param" --region "$REGION" --query 'Parameter.Value' --output text)
    local web_acl_name=$(echo "$web_acl_arn" | cut -d'/' -f3)
    local web_acl_id=$(echo "$web_acl_arn" | cut -d'/' -f4)
    local web_acl_current_rules=$(aws wafv2 get-web-acl --id "$web_acl_id" --name "$web_acl_name" --scope "$SCOPE" --region "$REGION" | jq -r '.WebACL.Rules')
    local web_acl_lock_token=$(aws wafv2 get-web-acl --id "$web_acl_id" --name "$web_acl_name" --scope "$SCOPE" --region "$REGION" | jq -r '.LockToken')

    aws wafv2 update-web-acl \
    --id "$web_acl_id" \
    --scope "$SCOPE" \
    --region "$REGION" \
    --name "$web_acl_name" \
    --default-action '{"Block": {"CustomResponse": {"CustomResponseBodyKey": "MAINTENANCE", "ResponseCode": 503}}}' \
    --visibility-config SampledRequestsEnabled=false,CloudWatchMetricsEnabled=false,MetricName="$web_acl_name" \
    --custom-response-bodies ./maintenance/jsons/custom_response_bodies.json \
    --rules "$web_acl_current_rules" \
    --lock-token "$web_acl_lock_token"
}

check_ip_in_ipset
add_ip_to_ipset
update_maintenance_rule_group
add_custom_response_to_web_acl

echo "Maintenance mode rule added."

end

'start_maintenance.bash'
#!/usr/bin/env bash
set -eux

# 必要な情報をセット
SCOPE="REGIONAL"
REGION="ap-northeast-1"

# メンテナンスグループの中身を空にする
clear_maintenance_rule_group() {
    local maintenance_rule_group_arn_param="/maintenance-rule-group-arn"
    local maintenance_rule_group_arn=$(aws ssm get-parameter --name "$maintenance_rule_group_arn_param" --region "$REGION" --query 'Parameter.Value' --output text)
    local maintenance_rule_group_name=$(echo "$maintenance_rule_group_arn" | cut -d'/' -f3)
    local maintenance_rule_group_id=$(echo "$maintenance_rule_group_arn" | cut -d'/' -f4)
    local maintenance_rule_group_lock_token=$(aws wafv2 get-rule-group --id "$maintenance_rule_group_id" --name "$maintenance_rule_group_name" --scope "$SCOPE" --region "$REGION" --output json | jq -r '.LockToken')
    local rule_group=$(aws wafv2 get-rule-group --id "$maintenance_rule_group_id" --name "$maintenance_rule_group_name" --scope "$SCOPE" --region "$REGION")

    # メンテナンスルールグループの中身が既に空の場合は処理終了
    if  echo "$rule_group" | jq -r '.RuleGroup.Rules' | grep -q '^\[\]$'; then
        echo "Error: Maintenance mode is not started." >&2
        exit 1
    fi
    
    aws wafv2 update-rule-group \
        --id "$maintenance_rule_group_id" \
        --name "$maintenance_rule_group_name" \
        --scope "$SCOPE" \
        --region "$REGION" \
        --lock-token "$maintenance_rule_group_lock_token" \
        --rules '[]' \
        --visibility-config SampledRequestsEnabled=false,CloudWatchMetricsEnabled=false,MetricName="$maintenance_rule_group_name"
}

# WebACLデータ取得して、カスタムレスポンスを削除する
clear_custom_response() {
    local web_acl_arn_param="/web-acl-arn"
    local web_acl_arn=$(aws ssm get-parameter --name "$web_acl_arn_param" --region "$REGION" --query 'Parameter.Value' --output text)
    local web_acl_name=$(echo "$web_acl_arn" | cut -d'/' -f3)
    local web_acl_id=$(echo "$web_acl_arn" | cut -d'/' -f4)
    local web_acl_current_rules=$(aws wafv2 get-web-acl --id "$web_acl_id" --name "$web_acl_name" --scope "$SCOPE" --region "$REGION" | jq -c '.WebACL.Rules')
    local web_acl_lock_token=$(aws wafv2 get-web-acl --id "$web_acl_id" --scope "$SCOPE" --region "$REGION" --name "$web_acl_name" --output json | jq -r '.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 $web_acl_current_rules \
        --lock-token $web_acl_lock_token
}

# IPSetからIPアドレスを全て削除
clear_ip_set() {
    # IPSetのARNを取得
    local ip_set_arn_param="/allow-ip-set-for-maintenance-arn"
    local ip_set_arn=$(aws ssm get-parameter --name "$ip_set_arn_param" --region "$REGION" --query 'Parameter.Value' --output text)
    local ip_set_name=$(echo "$ip_set_arn" | cut -d'/' -f3)
    local ip_set_id=$(echo "$ip_set_arn" | cut -d'/' -f4)

    local ip_set_lock_token=$(aws wafv2 get-ip-set --id "$ip_set_id" --scope "$SCOPE" --region "$REGION" --name "$ip_set_name" --output json | jq -r '.LockToken')

    aws wafv2 update-ip-set \
        --scope "$SCOPE" \
        --id "$ip_set_id" \
        --lock-token "$ip_set_lock_token" \
        --name "$ip_set_name" \
        --addresses '[]' \
        --region "$REGION"
}

# メインフロー
clear_maintenance_rule_group
clear_custom_response
clear_ip_set

echo "Maintenance mode rule deleted."

jsons

'custom_response_bodies.json'
{
  "MAINTENANCE": {
    "Content": "{\"message\":\"The service is under maintenance\"}",
    "ContentType": "APPLICATION_JSON"
  }
}

'maintenance_mode_rule.json'
{
  "Name": "allow-ip-address-rule",
  "Priority": 0,
  "Statement": {
      "IPSetReferenceStatement": {
        "ARN": "{{IP_SET_ARN}}"
      }
  },
  "Action": {
      "Allow": {}
  },
  "VisibilityConfig": {
      "SampledRequestsEnabled": true,
      "CloudWatchMetricsEnabled": true,
      "MetricName": "allow-ip-address-rule"
  }
}

参考

13
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?