AWS 公式テンプレートを使って CloudFormation で実装する ELB の LCU 予約と自動スケジューリングをやってみた
はじめに
テクニカルサポートの 片方 です。
今回は AWS 公式 GitHub 記載、YAML 形式のテンプレートを利用して、ELB の LCU 予約と自動スケジューリングの実装 を CloudFormation でやってみました。
ELB は負荷に合わせてスケールする機能がありますが ELB のスケールにはある程度の時間がかかり リクエストが瞬間的に増えたときは ELB のスケールが間に合わないことがあり、その際 ELB は HTTP 503 を返します。
そのため従来の回避策として、あらかじめ TV やメディアによる露出など急激なアクセス増が予想される場合に ELB の Prewarm リクエスト(暖気申請)をサポートケースにて申請する必要がございました。
現在は、サポートケースへ暖気申請することなく、ロードバランサーキャパシティユニット(LCU)予約 を活用してトラフィック急増への対処が可能です。
一方で、Prewarm リクエスト(暖気申請)と異なり、期間指定でスケールさせることは叶いません。そのため期間を指定する必要がある場合 EventBridge Scheduler + Lambda 等を利⽤した構成が考えられますが、
本ブログで紹介する AWS 公式 YAML 形式のテンプレートを利用すると、ELB の LCU 予約と自動スケジューリングが実装可能です。(CloudFormation を利用して実装)
注意点
もし、引き上げていない場合は LCU 予約が叶わず、CloudTrail イベント履歴 ModifyCapacityReservation より以下のエラーが発生することが想定されます。
"errorCode": "CapacityUnitsLimitExceededException",
"errorMessage": "For load balancer type 'application`, CapacityUnits exceeds the maximum value of '0`.",
また、後述する EMAIL アドレスを記載している場合、SNS より以下の通知がされます。(一部マスクします)
{"status": "FAILURE", "message": "Failed to modify capacity reservation for ELB: arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxx:loadbalancer/app/ALB-scheduler-lcu-reservation/23bac998ff3d9c83. Error: An error occurred (CapacityUnitsLimitExceeded) when calling the ModifyCapacityReservation operation: For load balancer type 'application`, CapacityUnits exceeds the maximum value of '0`.", "elb_arn": "arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxx:loadbalancer/app/ALB-scheduler-lcu-reservation/23bac998ff3d9c83", "desired_lcu": 100}
※私は引き上げを忘れた為、発生しました
今回は検証のため、予約するロードバランサーキャパシティユニット(LCU)の量を "100" で行いました。
本番環境では、PeakLCUs (必要な最⼤容量を⽰すメトリクス) をご参考に、過去の ALB 使⽤状況を加味して予約するべき LCU 数を⾒積りください。
PeakLCUs
特定の時点でロードバランサーが使用するロードバランサーキャパシティユニット (LCU) の最大数。LCU 予約を使用する場合にのみ適用されます。
ReservedLCUs
LCUs 予約を使用してロードバランサー用に予約されたロードバランサーキャパシティユニット (LCU) の数。
capacity-reservation-scheduler.yml について
CloudFormation で展開するため、GitHub より YAML 形式の当該ファイルをダウンロードしてください。
capacity-reservation-scheduler.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: AWS CloudFormation Template for ELB Capacity Unit Reservation scheduling
Parameters:
LoadBalancerArn:
Type: String
Description: The ARN of the ALB or NLB to reserve capacity
AllowedPattern: '^arn:(aws|aws-cn|aws-us-gov):elasticloadbalancing:[a-z0-9-]+:\d{12}:loadbalancer/(app|net)/[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$'
ProvisionedCapacityScheduleStart:
Type: String
Default: "0 12 * * ? *"
Description: The schedule in Cron format - https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-scheduled-rule-pattern.html
ProvisionedCapacityScheduleStop:
Type: String
Default: "0 13 * * ? *"
Description: The schedule in Cron format - https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-scheduled-rule-pattern.html
StartDate:
Type: String
Description: The date, in UTC (yyyy-MM-ddTHH:mm:ss.SSSZ), before which the schedule will start (immediate if blank)
EndDate:
Type: String
Description: The date, in UTC (yyyy-MM-ddTHH:mm:ss.SSSZ), before which the schedule will end (ignored if blank)
CapacityReservation:
Type: Number
Default: 100
MinValue: 100 # min NLB=5500 ALB=100
MaxValue: 99999 # max NLB=15000 ALB=1500
Description: Ammount of Load Balancer Capacity Units (LCU) to provision
CapacityReset:
Type: Number
Default: 0
MinValue: 0 # min NLB=5500 ALB=100
MaxValue: 99999 # max NLB=15000 ALB=1500
Description: Set capacity reservation to original value (or 0 to reset)
TimeZone:
Type: String
Description: Time Zone - https://www.iana.org/time-zones
Default: UTC
ScheduleGroup:
Type: String
Description: Specify ScheduleGroup (if left blank, one will be created)
Tag:
Type: String
Description: Tag to associate with resources. Thee ELB name will be used if left blank.
NotificationEmail:
Type: String
Description: Email for notifications (via SNS)
AllowedPattern: '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
Conditions:
CreateScheduleGroup: !Equals [!Ref ScheduleGroup, ""]
HasNotificationEmail: !Not [!Equals ['', !Ref NotificationEmail]]
HasTag: !Not [!Equals ['', !Ref Tag]]
HasStartDate: !Not [!Equals ['', !Ref StartDate]]
HasEndDate: !Not [!Equals ['', !Ref EndDate]]
Resources:
EventBridgeSchedulerExecRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub
- "${AWS::StackName}-eventbridge-${LoadBalancerName}"
- LoadBalancerName: !Select [2, !Split ["/", !Select [5, !Split [":", !Ref LoadBalancerArn]]]]
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- scheduler.amazonaws.com
Action:
- 'sts:AssumeRole'
Description: Execution role for the EventBridge scheduler lambda target invocation.
Policies:
- PolicyName: InvokeSchedulerLambda
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- 'lambda:InvokeFunction'
Resource:
- !GetAtt LambdaSetProvisionCapacity.Arn
Tags:
- Key: LoadBalancerName
Value: !Sub
- "${AWS::StackName}-${LoadBalancerName}"
- LoadBalancerName: !Select [2, !Split ["/", !Select [5, !Split [":", !Ref LoadBalancerArn]]]]
ProvisionCapacityScheduleGroup:
Condition: CreateScheduleGroup
Type: AWS::Scheduler::ScheduleGroup
Properties:
Name: !Sub ${AWS::StackName}-ScheduleGroup
ProvisionCapacityScheduleStart:
Type: AWS::Scheduler::Schedule
Properties:
Name: !Sub ${AWS::StackName}-PCSchedulerStart
Description: This schedule runs the Lambda function to start LCU Capacity Reservation
StartDate: !If [HasStartDate, !Ref StartDate, !Ref "AWS::NoValue"]
EndDate: !If [HasEndDate, !Ref EndDate, !Ref "AWS::NoValue"]
FlexibleTimeWindow:
Mode: "FLEXIBLE"
MaximumWindowInMinutes: 1
GroupName: !If [CreateScheduleGroup, !Ref ProvisionCapacityScheduleGroup, !Ref ScheduleGroup]
ScheduleExpression: !Sub "cron(${ProvisionedCapacityScheduleStart})"
ScheduleExpressionTimezone: !Ref TimeZone
State: ENABLED
Target:
Arn: !GetAtt LambdaSetProvisionCapacity.Arn
Input: !Sub '{"ELB": "${LoadBalancerArn}", "DesiredLCU": ${CapacityReservation}}'
RoleArn: !GetAtt EventBridgeSchedulerExecRole.Arn
RetryPolicy:
MaximumEventAgeInSeconds: 60
MaximumRetryAttempts: 10
ProvisionCapacityScheduleStop:
Type: AWS::Scheduler::Schedule
Properties:
Name: !Sub ${AWS::StackName}-PCSchedulerStop
Description: This schedule runs the Lambda function to stop LCU Capacity Reservation
StartDate: !If [HasStartDate, !Ref StartDate, !Ref "AWS::NoValue"]
EndDate: !If [HasEndDate, !Ref EndDate, !Ref "AWS::NoValue"]
FlexibleTimeWindow:
Mode: "FLEXIBLE"
MaximumWindowInMinutes: 1
GroupName: !If [CreateScheduleGroup, !Ref ProvisionCapacityScheduleGroup, !Ref ScheduleGroup]
ScheduleExpression: !Sub "cron(${ProvisionedCapacityScheduleStop})"
ScheduleExpressionTimezone: !Ref TimeZone
State: ENABLED
Target:
Arn: !GetAtt LambdaSetProvisionCapacity.Arn
Input: !Sub '{"ELB": "${LoadBalancerArn}", "DesiredLCU":${CapacityReset}}'
RoleArn: !GetAtt EventBridgeSchedulerExecRole.Arn
RetryPolicy:
MaximumEventAgeInSeconds: 60
MaximumRetryAttempts: 10
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub
- "${AWS::StackName}-lambdaexec-${LoadBalancerName}"
- LoadBalancerName: !Select [2, !Split ["/", !Select [5, !Split [":", !Ref LoadBalancerArn]]]]
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: LambdaExecutionPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: arn:aws:logs:*:*:*
- Effect: Allow
Action:
- elasticloadbalancing:ModifyCapacityReservation
Resource: !Ref LoadBalancerArn
- Effect: Allow
Action:
- elasticloadbalancing:DescribeCapacityReservation
Resource: '*'
- !If
- HasNotificationEmail
- Effect: Allow
Action:
- sns:Publish
Resource: !Ref NotificationTopic
- !Ref AWS::NoValue
NotificationTopic:
Type: AWS::SNS::Topic
Condition: HasNotificationEmail
Properties:
DisplayName: !Sub
- "${AWS::StackName}-${LoadBalancerName}"
- LoadBalancerName: !Select [2, !Split ["/", !Select [5, !Split [":", !Ref LoadBalancerArn]]]]
TopicName: !Sub
- "${AWS::StackName}-${LoadBalancerName}"
- LoadBalancerName: !Select [2, !Split ["/", !Select [5, !Split [":", !Ref LoadBalancerArn]]]]
Subscription:
- Endpoint: !Ref NotificationEmail
Protocol: email
Tags:
- Key: LoadBalancerName
Value: !If [HasTag, !Ref Tag, !Select [2, !Split ["/", !Select [5, !Split [":", !Ref LoadBalancerArn]]]]]
LambdaSetProvisionCapacity:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub
- "${AWS::StackName}-${LoadBalancerName}"
- LoadBalancerName: !Select [2, !Split ["/", !Select [5, !Split [":", !Ref LoadBalancerArn]]]]
Description: !Sub
- "Reserved Capacity function for ELB: ${LoadBalancerName}"
- LoadBalancerName: !Select [5, !Split [":", !Ref LoadBalancerArn]]
Code:
ZipFile: |
import os
import json
import logging
import time
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# Temporary until Lambda runtime supports newer boto3 versions
def _install_boto3():
import sys
os.system("mkdir -p /tmp/packages")
logger.info("Installling boto3")
os.system(
f"{sys.executable} -m pip install "
f"--no-cache-dir --target /tmp/packages "
f"--only-binary :all: --no-color "
f"--no-warn-script-location --upgrade boto3==1.35.68")
sys.path.insert(0, "/tmp/packages")
def lambda_handler(event, context):
_install_boto3()
import boto3
logger.info(f"event: {event}")
logger.info(f"boto3 version: {boto3.__version__}")
# Extract parameters from the event
elb_arn = event.get('ELB')
desired_lcu = event.get('DesiredLCU')
# Get SNS Topic ARN from environment variable
sns_topic_arn = os.environ.get('SNS_TOPIC_ARN')
reset_capacity_reservation = True if desired_lcu == 0 else False
elbv2_client = boto3.client('elbv2')
sns_client = boto3.client('sns')
try:
# Create base parameters
params = {
'LoadBalancerArn': elb_arn,
'ResetCapacityReservation': reset_capacity_reservation
}
# Add MinimumLoadBalancerCapacity only if desired_lcu is not 0
if desired_lcu != 0:
params['MinimumLoadBalancerCapacity'] = {
'CapacityUnits': desired_lcu
}
# Make the API call with the constructed parameters
response = elbv2_client.modify_capacity_reservation(**params)
# Wait for capacity reservation to be provisioned or failed
while True:
response = elbv2_client.describe_capacity_reservation(
LoadBalancerArn=elb_arn
)
logger.info(f"describe_capacity_reservation response: {response}")
if response['CapacityReservationState']:
status_code = response['CapacityReservationState'][0]['State']['Code']
if status_code in ['provisioned', 'failed']:
break
time.sleep(5)
if status_code == 'provisioned':
status = "SUCCESS"
message = f"Successfully modified and provisioned capacity reservation for ELB: {elb_arn}"
else:
status = "FAILURE"
message = f"Failed to provision capacity reservation for ELB: {elb_arn}"
logger.info(f"Status: {status}, Message: {message}")
except Exception as e:
status = "FAILURE"
message = f"Failed to modify capacity reservation for ELB: {elb_arn}. Error: {str(e)}"
logger.info(f"Status: {status}, Message: {message}")
# Prepare SNS message
sns_message = {
"status": status,
"message": message,
"elb_arn": elb_arn,
"desired_lcu": desired_lcu
}
# Send SNS notification if SNS_TOPIC_ARN is provided
if sns_topic_arn:
try:
sns_client.publish(
TopicArn=sns_topic_arn,
Message=json.dumps(sns_message),
Subject=f"ELB Capacity Modification {status}"
)
logger.info(f"SNS notification sent: {json.dumps(sns_message)}")
except Exception as e:
logger.info(f"Failed to send SNS notification. Error: {str(e)}")
else:
logger.info("SNS_TOPIC_ARN not provided. Skipping SNS notification.")
return {
"statusCode": 200 if status == "SUCCESS" else 500,
"body": json.dumps({"status": status, "message": message})
}
Handler: "index.lambda_handler"
Runtime: python3.13
Timeout: 900
Role: !GetAtt LambdaExecutionRole.Arn
Environment:
Variables:
SNS_TOPIC_ARN: !If [HasNotificationEmail, !Ref NotificationTopic, !Ref "AWS::NoValue"]
Tags:
- Key: LoadBalancerName
Value: !If [HasTag, !Ref Tag, !Select [2, !Split ["/", !Select [5, !Split [":", !Ref LoadBalancerArn]]]]]
パラメーター
CloudFormation でのスタック作成時には上からの順に、以下のパラメーターを指定します。
-
CapacityReservation
予約するロードバランサーキャパシティユニット(LCU)の量を指定します。
PeakLCUs メトリクス値などを参考に、適切な値を設定してください。 -
CapacityReset
LCU 予約終了時に設定する値です。デフォルト: 0
通常 0 で使用するケースが多いことが想定されます。 -
EndDate [オプション]
スケジュール全体の終了日時(空白の場合は無期限) -
LoadBalancerArn
LCU 予約 ALB または NLB の ARN -
NotificationEmail [オプション]
Email アドレスを記載すると、こちらをエンドポイントにしたサブスクリプションと SNS トピックが自動で作成されます。 -
ProvisionedCapacityScheduleStart
各日の LCU 予約開始時刻(cron式) -
ProvisionedCapacityScheduleStop
各日の LCU 予約停止時刻(cron式)
ProvisionedCapacitySchedule (Start/Stop) は eventbridge のスケジュール機能で制御します。cron 式の記述方法は以下をご参考ください。
-
ScheduleGroup [オプション]
スケジュールグループがある場合は指定します。存在しない場合は空白のままにすることで、新しいスケジュールグループが作成されます。 -
StartDate [オプション]
スケジュール全体の開始日時(空白の場合は即時開始) -
Tag [オプション]
リソースに関連付けるタグを指定します。空白の場合は ELB 名が使用されます。 -
TimeZone [オプション]
タイムゾーンを記載。デフォルト: UTC
やってみた
※ CloudFormation で展開するため、GitHub より YAML 形式の当該ファイルをダウンロードしてください。
https://console.aws.amazon.com/cloudformation で AWS CloudFormation コンソール を開きます。
画面の上部のナビゲーションバーで、スタックを作成する AWS リージョンを選択します。
[スタック] ページでは、右上の [スタックの作成] を選択してから、[新しいリソースを使用 (標準)] を選択します。
既存のテンプレートを選択し、テンプレートの指定では、テンプレートファイルのアップロードを選択します。
その後、該当の YAML 形式のファイルを選択します。
前途で紹介した内容を参考に、パラメーターを指定し完了します。
次のページでは、特段なにも設定は行わず、下部へスクロールしてチェックボックスにチェックを入れます。
次のページで、パラメーター値などを確認して問題なければ [送信] を押します。
これで完了です!非常に簡単ですね。
もし、パラメーターの NotificationEmail 欄に記載をした場合は、サブスクリプションの確認をしてください。
確認してみた
対象 ELB、arn に指定した ALB を確認します。
以下の通り、指定した日時で 100 LCU が予約されており、2 AZ に対して 50 づつ予約されていました。
また、指定した終了日時を過ぎると、以下の通り自動で解除されていました。
CloudWatch メトリクスも確認したところ、想定通りに増加しているので成功しています。
なお、LCU 予約が成功した場合、SNS より "ELB Capacity Modification SUCCESS" というタイトルで以下の通知がされます。(一部マスクします)
{"status": "SUCCESS", "message": "Successfully modified and provisioned capacity reservation for ELB: arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxx:loadbalancer/app/ALB-scheduler-lcu-reservation/23bac998ff3d9c83", "elb_arn": "arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxx:loadbalancer/app/ALB-scheduler-lcu-reservation/23bac998ff3d9c83", "desired_lcu": 100}
まとめ
非常に便利で簡単に実装できますね。本ブログが誰かの参考になれば幸いです。
参考資料
- Elastic Load Balancing の暖気申請について | DevelopersIO
- ロードバランサーキャパシティユニット(LCU)予約を活用したトラフィック急増への備え | Amazon Web Services ブログ
- Application Load Balancer のロードバランサーキャパシティユニット予約 - エラスティックロードバランシング
- elastic-load-balancing-tools/scheduler-lcu-reservation at master · aws/elastic-load-balancing-tools · GitHub
- Application Load Balancer の CloudWatch メトリクス - エラスティックロードバランシング
- cron 式と rate 式を使用して Amazon EventBridge でルールをスケジュールする - Amazon EventBridge
- 新機能のロードバランサーキャパシティユニット (LCU) 予約で、ALBの暖機を試みてみた | DevelopersIO
アノテーション株式会社について
アノテーション株式会社は、クラスメソッド社のグループ企業として「オペレーション・エクセレンス」を担える企業を目指してチャレンジを続けています。「らしく働く、らしく生きる」のスローガンを掲げ、様々な背景をもつ多様なメンバーが自由度の高い働き方を通してお客様へサービスを提供し続けてきました。現在当社では一緒に会社を盛り上げていただけるメンバーを募集中です。少しでもご興味あれば、アノテーション株式会社WEBサイトをご覧ください。