#はじめに
キャンペーン等でWebサーバに対して想定数以上のアクセスが発生した場合に、Webサーバが高負荷となりレスポンスが悪化することがあると思います。そのような場合には、負荷を低減するために、一定数以上のアクセスを抑止する方法が考えられます。(他には、"Webサーバの前段に高機能のロードバランサを導入しアクセスを抑止"したり、"Webサーバをスケールアウトする"方法もあります)
今回、AWS ALB(Application Load Balancer)及びAWSマネージメントサービスのみでこれを実装してみました。
なお、調査した限り、実際には"一定数以上のアクセスを抑止する"といったことはできませんでした(AWS SA様へも確認済)。代わりの方法として、特定パスに対するアクセスを一時的に抑止することで、Webサイト全体のレスポンス悪化を回避する方法を採用しています。
#環境
構成と抑止の挙動は以下の通りです。
####前提
- (キャンペーン等で)アクセスの増加が見込まれるコンテンツが事前に把握できている。今回の対象コンテンツを「campaign.html」とする。
- 抑止をトリガするためのメトリクスをALB「NewConnectionCount※」とする
※NewConnectionCount:クライアントからロードバランサーへ、およびロードバランサーからターゲットへの、新たに確立された TCP 接続の総数。
#構築
- VPC、ALB、Webサーバは既に構築済みとします。
※これらを含む、環境構築用CloudFormationテンプレートは最後の補足を参照してください。
動作確認
環境構築用CloudFormationを設定後、以下URLへアクセスし、トップページ及びキャンペーンページ・非キャンペーンページが表示することを確認します。
「campaign.html」向けALBリスナールールの作成
後述のLambda Functionから制御するため、事前にリスナールールを作成します。ルール内容は、「転送」とします。
-
AWSマネジメントコンソールから
[EC2]-[ロードバランサ]-[{今回作成したALB}]-[リスナー]-[デフォルト:転送先 test-siteルールの表示/編集]の順にクリック
-
合わせて、今回作成したルールのARNをコピーし控える。※Lambda Function作成時に使用するため
また、今回作成したTarget GroupのARNもコピーし控える。※Lambda Function作成時に使用するため
IAM Roleの作成
Lambda Function用のIAM Roleを作成します。また、ALBリスナールールを修正するために必要な権限を付与します。
- AWSマネジメントコンソールから
[IAM]-[アクセス管理]-[ロール]の順にクリック
[ロールの作成]をクリック後、[ロールの作成]では[AWSサービス]、[Lambda]を選択し、[次のステップ]をクリックします。
- [ElasticLoadBalancingFullAccess]を選択し、[次のステップ]をクリック
※検証のため緩い権限としています。本番導入時にはベストプラクティスに沿って最小権限としましょう。
- [タグの追加]は何もせず、[次のステップ]をクリック
ロール名は(任意名)"TestALBRateLimitIAMRole"を入力し、[ロールの作成]をクリック
Lambda Functionの作成
CloudWatch Alarmの状態に応じて上記リスナールールを修正するLambda Functionを2つ作成します。
1つ目(Alarmが発火(ALARM)) ※リスナールールを"固定レスポンスを返す(503)"へ修正
-
AWSマネジメントコンソールから
[Lambda]-[関数]-[関数の作成]の順にクリック
「関数の作成」では以下の流れで設定し、[関数の作成]をクリック -
関数名:(任意)TestALBRateLimitALARMFunction
-
ランタイム:Python 3.6
-
[コード]-[コードソース]から[lambda_function.py]をダブルクリックし、中身を以下内容で上書き後、[Deploy]をクリック
"RuleArn"は上記で控えたALBリスナールールのARNを指定
※AWS SDK for Python (Boto3)を使用
参考:https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/elbv2.html#ElasticLoadBalancingv2.Client.modify_rule
import boto3
def lambda_handler(event, context):
client = boto3.client('elbv2')
response = client.modify_rule(
RuleArn='arn:aws:elasticloadbalancing:us-east-1:{accont id}:listener-rule/{........}',
Actions=[
{
'Type': 'fixed-response',
'FixedResponseConfig': {
'StatusCode': '503'
}
}
]
)
2つ目(Alarmが沈静(OK)) ※リスナールールを"転送"へ修正
1つ目と同様に進めます。
-
[Lambda]-[関数]-[関数の作成]の順にクリック
「関数の作成」では以下の流れで設定し、[関数の作成]をクリック -
関数名:(任意)TestALBRateLimitOKFunction
-
ランタイム:Python 3.6
-
アクセス権限:[既存のロールを使用する]-[TestALBRateLimitIAMRole]
-
[コード]-[コードソース]から[lambda_function.py]をダブルクリックし、中身を以下内容で上書き後、[Deploy]をクリック
"RuleArn"は上記で控えたALBリスナールールのARNを指定
"TargetGroupArn"は上記で控えたALB Target GroupのARNを指定
import boto3
def lambda_handler(event, context):
client = boto3.client('elbv2')
response = client.modify_rule(
RuleArn='arn:aws:elasticloadbalancing:us-east-1:{accont id}:listener-rule/{........}',
Actions=[
{
'Type': 'forward',
'TargetGroupArn': 'arn:aws:elasticloadbalancing:us-east-1:{accont id}:targetgroup/{........}'
}
]
)
CloudWatch Alarmの作成
メトリクスALB「NewConnectionCount」の閾値を設定します。テストを容易にするために低い数字を設定します。
-
AWSマネジメントコンソールから
[CloudWatch]-[アラーム]-[アラームの作成]の順にクリック
[メトリクスの選択]をクリック
[メトリクス]から[ApplicationELB]-[AppELB別メトリクス]-[{今回作成したLoadBalancer}の"NewConnectionCount"]へチェックを入れ、[メトリクスの選択]をクリック -
[条件]から[...よりも]へ"5"を入力
-
[その他の設定]-[欠落データの処理]を[欠損データを適性(しきい値をこえていない)として処理]へ選択し、[次へ]をクリック
-
[アラーム名]へ(任意名)"Test ALB Rate Limit"と入力し、[次へ]をクリック
-
[アラームの作成]をクリック
合わせて、今回作成したアラームのARNをコピーし控えておく。※CloudWatch Event作成時に使用するため
CloudWatch Eventの作成(2つ)
CloudWatch Alarmの状態に応じてEventを2つ作成します。
1つ目(Alarmが発火(ALARM))
- [CloudWatch]-[イベント]-[ルール]-[ルールの作成]の順にクリック
- イベントソースは[イベントパターンのプレビュー]右の[編集]をクリックし以下内容を入力後、[保存]をクリック
"resources"は上記で控えたアラームのARNを指定
※detail.state.valueは"ALARM"を指定
{
"source": [
"aws.cloudwatch"
],
"detail-type": [
"CloudWatch Alarm State Change"
],
"resources": [
"arn:aws:cloudwatch:us-east-1:{accont id}:alarm:Test ALB Rate Limit"
],
"detail": {
"state": {
"value": [
"ALARM"
]
}
}
}
2つ目(Alarmが沈静(OK))
- [CloudWatch]-[イベント]-[ルール]-[ルールの作成]の順にクリック
イベントソースは[イベントパターンのプレビュー]右の[編集]をクリックし以下内容を入力後、[保存]をクリック
"resources"は上記で控えたアラームのARNを指定
※detail.state.valueは"OK"を指定
{
"source": [
"aws.cloudwatch"
],
"detail-type": [
"CloudWatch Alarm State Change"
],
"resources": [
"arn:aws:cloudwatch:us-east-1:{accont id}:alarm:Test ALB Rate Limit"
],
"detail": {
"state": {
"value": [
"OK"
]
}
}
}
動作確認
正常時(転送)
環境構築用CloudFormation設定から数分待ってから、以下URLへアクセスし、トップページ及びキャンペーンページ・非キャンペーンページが表示することを確認します。
http://{ALB Public DNS}/index.html
アクセス増(抑止)
各トップページ及びキャンペーンページ・非キャンペーンページへアクセスし、5回程度リロードします。
トップページ: http://{ALB Public DNS}/index.html
キャンペーンページ: http://{ALB Public DNS}/campaign.html
非キャンペーンページ: http://{ALB Public DNS}/not_campaign.html
約1-2分後に各ページへアクセスすると「キャンペーンページ(http://{ALB Public DNS}/campaign.html)」のみ何も表示されない(ステータスコード:503)ことが確認できます。
※キャッシュが残っていると表示するため、その場合、キャッシュを削除するためにスーパーリロード(Ctrl+F5 or Ctrl+Shift+F5)を実施します。
そのまま何もせず、さらに約1-2分後に「キャンペーンページ(http://{ALB Public DNS}/campaign.html)」へアクセスするとページが表示する(ステータスコード:200)ことが確認できます。
#まとめ
ALB,CloudWatch Alarm/Event,Lambdaを使用して流量制限を行いました。
なお、他の条件で抑止を行いたい場合は、各設定をカスタマイズすることで対応することができます。
「はじめに」に記載しましたが、"一定数以上のアクセスを抑止する"といった方法を見つけることができませんでした。良い方法がありましたら教えて頂ければ幸いです。
#補足
- 環境構築用CloudFormationテンプレート
AWSTemplateFormatVersion: 2010-09-09
Description: EC2 for Web Server
Parameters:
ParamTag:
Type: String
Default: test-site
ParamAMI:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
ParamCidrBlock:
Type: String
Default: "10.0.0.0/16"
Resources:
CFnVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref ParamCidrBlock
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Ref ParamTag
CfnVPCIGW:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Ref ParamTag
CfnAttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref CFnVPC
InternetGatewayId: !Ref CfnVPCIGW
CfnNatGatewayEIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
CfnAttachNatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt CfnNatGatewayEIP.AllocationId
SubnetId: !Ref CfnPublicSubnet1
CfnPublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref CFnVPC
Tags:
- Key: Name
Value: !Join [ "-", [ !Ref ParamTag, public ] ]
CfnPublicRT:
DependsOn: CfnVPCIGW
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref CfnPublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref CfnVPCIGW
CfnPublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 0, !GetAZs ]
VpcId: !Ref CFnVPC
CidrBlock: !Select [ 0, !Cidr [ !Ref ParamCidrBlock, 4, 8 ]]
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Join [ "-", [ !Ref ParamTag, public, a ] ]
CfnPublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 1, !GetAZs ]
VpcId: !Ref CFnVPC
CidrBlock: !Select [ 1, !Cidr [ !Ref ParamCidrBlock, 4, 8 ]]
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Join [ "-", [ !Ref ParamTag, public, b ] ]
CfnPublicSubnet1Association:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref CfnPublicRouteTable
SubnetId: !Ref CfnPublicSubnet1
CfnPublicSubnet2Association:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref CfnPublicRouteTable
SubnetId: !Ref CfnPublicSubnet2
CfnPrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref CFnVPC
Tags:
- Key: Name
Value: !Join [ "-", [ !Ref ParamTag, private ] ]
CfnPrivateRT:
DependsOn: CfnVPCIGW
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref CfnPrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref CfnAttachNatGateway
CfnPrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 0, !GetAZs ]
VpcId: !Ref CFnVPC
CidrBlock: !Select [ 2, !Cidr [ !Ref ParamCidrBlock, 4, 8 ]]
Tags:
- Key: Name
Value: !Join [ "-", [ !Ref ParamTag, private, a ] ]
CfnPrivateSubnet1Association:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref CfnPrivateRouteTable
SubnetId: !Ref CfnPrivateSubnet1
SecurityGroupWeb01:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: fw-ec2-web01
GroupDescription: fw-ec2-web01
VpcId: !Ref CFnVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: icmp
FromPort: -1
ToPort: -1
CidrIp: 0.0.0.0/0
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Ref ParamTag
CfnFrontLB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Ref ParamTag
Subnets:
- !Ref CfnPublicSubnet1
- !Ref CfnPublicSubnet2
SecurityGroups:
- !Ref SecurityGroupWeb01
Tags:
- Key: Name
Value: !Ref ParamTag
CfnFrontLBListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref CfnFrontLB
Port: 80
Protocol: HTTP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref CfnFrontLBTargetGroup
CfnFrontLBTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Ref ParamTag
VpcId: !Ref CFnVPC
Port: 80
Protocol: HTTP
HealthCheckPath: /index.html
HealthCheckIntervalSeconds: 5
HealthCheckTimeoutSeconds: 2
HealthyThresholdCount: 2
Targets:
- Id: !Ref CfnWeb01
Tags:
- Key: Name
Value: !Ref ParamTag
CfnWeb01:
DependsOn: CfnPrivateRT
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref ParamAMI
InstanceType: t2.micro
SubnetId: !Ref CfnPrivateSubnet1
SecurityGroupIds:
- !Ref SecurityGroupWeb01
UserData:
Fn::Base64: |
#!/bin/bash -xe
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo 'This is Top Page<p>' > /var/www/html/index.html
echo '<a href="campaign.html">Campaign</a><br>' >> /var/www/html/index.html
echo '<a href="not_campaign.html">Not Campaign</a><br>' >> /var/www/html/index.html
echo 'This is Campaign Page' > /var/www/html/campaign.html
echo 'This is not Campaign Page' > /var/www/html/not_campaign.html
Tags:
- Key: Name
Value: !Ref ParamTag
Outputs:
URL:
Value: !Join [ "", [ "http://", !GetAtt CfnFrontLB.DNSName,"/index.html" ] ]
- 環境構築用CloudFormationテンプレートのデプロイ
aws cloudformation validate-template --template-body file://001-BaseALBWeb.yml
aws cloudformation deploy --stack-name handson-alb-rate-limit --template-file 001-BaseALBWeb.yml --region us-east-1
- 削除手順
以下の順で削除を行います。 - CloudWatch Event
- CloudWatch Alarm
- Lambda Function
- ALB 追加したリスナールール
- CloudFormation Stack