はじめに
メンテナンスページの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にカスタムレスポンスを追加
- パラメータで渡したIPが、IPSetにすでにあるかの確認
- end
- メンテナンスグループの中身を空にする
- WebACLデータ取得して、カスタムレスポンスを削除する
- IPSetからIPアドレスを全て削除
- JSON(custom_response_bodies)
- メンテナンス時に返すレスポンスの中身
- JSON(maintenance_mode_rule)
- メンテナンス時、メンテナンスルールグループに入れるルールの中身(AllowIpAddress)
- start
ディレクトリ構成
ディレクトリの構成は以下の通りです。
┣ 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で保存
コード
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アドレスを追加して中止。されていなかったら続ける
- SSMの保存されたメンテナンスルールARNで既に適用されているか確認
- default-rule-groupを削除し、maintenance-rule-groupを適用
- IPSetARN取得
end
- パラメータ
- なし
- フロー
- メンテナンスグループの中身を空にする
- メンテナンスルールグループの中身が既に空の場合は処理終了
- WebACLデータ取得して、カスタムレスポンスを削除する
- IPSetのARNを取得して、IPSetからIPアドレスを全て削除
- メンテナンスグループの中身を空にする
コード
start
#!/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
#!/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
{
"MAINTENANCE": {
"Content": "{\"message\":\"The service is under maintenance\"}",
"ContentType": "APPLICATION_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"
}
}