はじめに
最近、AWS Well-Architected Tool
を利用して、AWS Well-Architecture Framework
からなるAWSのベストプラクティスに沿った設計および運用チェックを実施する機会があったのですが、その中で以下の設問がありました。
REL 1: サービスクォータと制約はどのように管理しますか?
上記設問に対する管理方法としては、上限のあるAWSリソースに対してCloudWatch Alarm
を利用して監視を行い、上限値に近づいたことを検知し、AWSサポートに上限緩和申請を出す方法を採用していました。
しかし、上記URLに記された改善計画に 「クォータ管理を自動化する」 方法が紹介されており、私の管理方法が古いことに気づかされてしまいました。。。
そこで今回は、管理方法改善の第一歩として、クォータ管理の自動化設定を実践してみたいと思います。
本記事の実施内容
利用ツール
今回は以下のツールを利用してAWSの環境構築及び動作検証を行います。
ツールの説明は本記事では割愛いたしますが、過去の記事で触れておりますので、もしご興味のある方はそちらをご参照ください。
-
AWS CDK
- AWSリソースを構築するために利用
-
Locust
- Lambda関数の同時実行数を上げる(負荷をかける)ために利用
構成図
Lambda関数の同時実行数を一時的に増やすためのRestAPIを構築し、Lambda関数の同時実行数が一定数を超過した場合にService Quotas
経由で上限緩和リクエストを送信するようにAWSリソースを準備します。
処理シーケンス
上記構成図のServiceQuotasStack
は以下のような処理を実装します。
今回は検証なのでCloudWatch Alarm
のアラート閾値は低め、かつLambda関数の処理はかなり簡素になっていますが、実運用する場合はそのあたりの検討する必要です。
(※)Lambdaの同時実行数の使用率…Lambda関数の同時実行数の上限値に対して、実際に使用した同時実行数を比較した際の割合を指します。
上限緩和自動化を検証
実施環境
以降に記した実施手順は、以下の動作環境で行っていきます。
$ cdk --version
2.65.0 (build 5862f7a)
$ aws --version
aws-cli/2.10.3 Python/3.9.11 Linux/5.10.16.3-microsoft-standard-WSL2 exe/x86_64.ubuntu.20 prompt/off
$ docker --version
Docker version 23.0.0, build e92dd87
$ docker-compose --version
docker-compose version 1.25.0, build unknown
$ uname -a
Linux MyPC 5.10.16.3-microsoft-standard-WSL2 #1 SMP Fri Apr 2 22:23:49 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
実施手順
- 上限緩和申請に必要な情報を事前に確認
Service Quotas
の管理画面から以下のカテゴリの「サービスコード」と「クォーターコード」を確認しておきます。 - RestAPI構築(スタック名:
RestApiStack
)
負荷試験ツール(Locust
)からLambda関数を起動させるために、Get
メソッドで単純なメッセージが返却される簡素なRestAPIを用意します。- ソースコード群
rest_api_stack.py
from aws_cdk import ( Stack, Duration, aws_lambda as lambda_, aws_lambda_event_sources as lambda_event_sources, ) from constructs import Construct function_timeout = 3 function_memory_size = 128 class RestApiStack(Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) lambda_function = lambda_.Function(self, "Function", code=lambda_.Code.from_asset("functions/get_hello_world"), handler="app.lambda_handler", runtime=lambda_.Runtime.PYTHON_3_8, timeout=Duration.minutes(function_timeout), memory_size=function_memory_size, ) lambda_function.add_event_source(lambda_event_sources.ApiEventSource( method="get", path="/hello-world", ) )
functions/get_hello_world/app.pyimport json def lambda_handler(event, context): return { "statusCode": 200, "body": json.dumps({ "message": "hello world from SAM and the CDK!", }), }
- デプロイコマンド
$ cdk deploy RestApiStack ... RestApiStack: creating CloudFormation changeset... ✅ RestApiStack ✨ Deployment time: 65.42s Outputs: RestApiStack.RestApiStackFunction50B5C7FCApiEventSourceA7A86A4FEndpointA8987CBD = https://{api_id}.execute-api.ap-northeast-1.amazonaws.com/prod/ Stack ARN: arn:aws:cloudformation:ap-northeast-1:{account_id}:stack/RestApiStack/b36f4530-e641-11ed-8e98-0ebbca01d889 ✨ Total time: 69.06s
- ソースコード群
- 上限緩和自動化処理構築(スタック名:
ServiceQuotaStack
)
1.にて確認した「サービスコード」と「クォーターコード」をFUNCTION_ENV_SERVICE_CODE
とFUNCTION_ENV_QUOTA_CODE
に含めて環境をデプロイします。(本来はパラメーターファイルなどに外だしすべきですが、いつも通り今回は横着してソースに直書きします。)
具体的な処理内容は 「処理シーケンス」 の章ですでに説明していますが、ソースコード内コメントにも簡単に説明を記述しておりますので、気になる方はご確認ください。- ソースコード群
service_quota_stack.py
from aws_cdk import ( Stack, Duration, aws_lambda as lambda_, aws_lambda_event_sources as lambda_event_sources, aws_cloudwatch as cloudwatch, aws_cloudwatch_actions as cloudwatch_actions, aws_sns as sns, aws_iam as iam, ) from constructs import Construct # Lambda関数のリソース設定 FUNCTION_TIMEOUT = 10 FUNCTION_MEMORY_SIZE = 128 # Lambda関数の環境変数 FUNCTION_ENV_SERVICE_CODE = "lambda" FUNCTION_ENV_QUOTA_CODE = "L-B99A9384" FUNCTION_ENV_MAX_DESIRED_VALUE = "100" FUNCTION_ENV_INCREASE_VALUE = "5" # CloudWatch Alarmのアラート閾値 ALART_THERESHLD_PERCENT = 10 class ServiceQuotasStack(Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # CloudWatch Alarmに設定するSNSトピックを作成 topic = sns.Topic(self, "Topic") # Service Quotasの申請を行うLambda関数作成 lambda_function = lambda_.Function(self, "Function", code=lambda_.Code.from_asset("functions/request_quota_increase"), handler="app.lambda_handler", runtime=lambda_.Runtime.PYTHON_3_8, timeout=Duration.minutes(FUNCTION_TIMEOUT), memory_size=FUNCTION_MEMORY_SIZE, environment={ "SERVICE_CODE": FUNCTION_ENV_SERVICE_CODE, "QUOTA_CODE": FUNCTION_ENV_QUOTA_CODE, "MAX_DESIRED_VALUE": FUNCTION_ENV_MAX_DESIRED_VALUE, "INCREASE_VALUE": FUNCTION_ENV_INCREASE_VALUE, } ) # Lambda関数にService Quotasの操作権限を付与 lambda_function.add_to_role_policy(iam.PolicyStatement( actions=[ "servicequotas:GetServiceQuota", "servicequotas:RequestServiceQuotaIncrease", ], resources=["*"]) ) # LambdaのトリガーにSNSトピックを設定 lambda_function.add_event_source(lambda_event_sources.SnsEventSource(topic)) # Lambda関数の同時実行数の使用率をCloudWatch Metricの数式で作成 concurrent_executions_rate = cloudwatch.MathExpression( expression="(usage_data/SERVICE_QUOTA(usage_data))*100", using_metrics={ "usage_data": cloudwatch.Metric( namespace="AWS/Lambda", metric_name="ConcurrentExecutions", period=Duration.minutes(1), statistic="Maximum", ) }, period=Duration.minutes(1), label="% 使用率", ) # 使用率が閾値を超過した場合、SNSトピックが起動するCloudWatch Alarmを作成 alarm = concurrent_executions_rate.create_alarm(self, "Alarm", comparison_operator=cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, threshold=ALART_THERESHLD_PERCENT, evaluation_periods=1, ) alarm.add_alarm_action(cloudwatch_actions.SnsAction(topic))
functions/request_quota_increase/app.pyimport os import boto3 SERVICE_CODE = os.environ.get("SERVICE_CODE") QUOTA_CODE = os.environ.get("QUOTA_CODE") MAX_DESIRED_VALUE = os.environ.get("MAX_DESIRED_VALUE") INCREASE_VALUE = os.environ.get("INCREASE_VALUE") client = boto3.client('service-quotas') def lambda_handler(event, context): try: # 監視対象のサービスクォータの情報を取得 service_quota_info = client.get_service_quota( ServiceCode=SERVICE_CODE, QuotaCode=QUOTA_CODE ) # 取得したサービスクォータの現在の値が自身が指定した最大値未満の場合、上限緩和申請を実施する service_quota_value = service_quota_info["Quota"]["Value"] if service_quota_value < int(MAX_DESIRED_VALUE): client.request_service_quota_increase( ServiceCode=SERVICE_CODE, QuotaCode=QUOTA_CODE, DesiredValue=service_quota_value + int(INCREASE_VALUE) ) print('Requested Service QuotaIncrease.') else: print('No requested.') except Exception as err: print('No requested.') print(err)
- デプロイコマンド
$ cdk deploy ServiceQuotaStack ... ServiceQuotaStack: creating CloudFormation changeset... ✅ ServiceQuotaStack ✨ Deployment time: 24.26s Stack ARN: arn:aws:cloudformation:ap-northeast-1:{account_id}:stack/ServiceQuotaStack/6e6cfc20-e2a9-11ed-9952-0a7b9b30f08f ✨ Total time: 27.98s
- ソースコード群
-
Locust
から2.で構築したRestAPIに対して負荷をかける
負荷の内容は特に凝ったことはせず、純粋にGUIでしていたRestAPIのHOSTにGet
メソッドで10
秒おきにアクセスするだけのテストコード(locustfile.py
)を用意しています。その他のソースコードおよび起動コマンドは過去に利用したものを流用しているだけですので、特に深い意味はありません。- ソースコード群
locustfile.py
from locust import HttpUser, task, constant class WebAccessUser(HttpUser): wait_time = constant(10) @task def web_access_success(self): self.client.get("")
DockerfileFROM locustio/locust WORKDIR /mnt/locust COPY *.py /mnt/locust/
docker-compose.ymlversion: '3' services: master: build: ./ ports: - "8089:8089" command: -f /mnt/locust/locustfile.py --master -P 8089 worker: build: ./ command: -f /mnt/locust/locustfile.py --worker --master-host master
- 起動コマンド
$ docker build $ docker-compose up --scale worker=2
- GUIにて負荷開始!(スクショのみ)
- ソースコード群
- マネージメントコンソールから想定通り動いているか確認
まずはService Quotas
の画面を確認。
4.で実施したタイミングで使用率が10%を超えている、かつクォータ増加リクエストが想定通りの値(現在の上限値50
+5
=55
)で申請されていることが確認できました。
念のためCloudWatch Alarm
の画面も確認すると、Service Quitas
のメトリクスと同時刻(2023-04-29 04:30ごろ)にSNS
へのアクションが実行されたことも確認できました。
- さらに時間経過で
Service Quotas
の画面を確認
記事をまとめている間(3時間程度経過後?)にもう一度Service Quotas
の画面を確認したところ、適用されたクォータ値が50
から55
に更新されていることも確認できました。
AWSサポート画面でも「対応したよ」というメッセージが確認できました。
これで検証は終了です。
ちなみにService Quotas
を確認した際には、リクエストから3時間程度で対応いただいた(Service Quotas
の画面上は変更されていた)のですが、サポートの返信はそれ以上の時間がかかっているようでした。
もしかすると上限緩和処理システムで自動化されていて、サポートの返信は人がやっている…などのことがあるのかなと思いました。
補足
CloudWatch Alarm
の数式で利用したSERVICE_QUOTA
関数について
実は、スタック名ServiceQuotaStack
の中で定義したCloudWatch Alarm
の数式は以下のようにSERVICE_QUOTA
関数を使用して使用率を算出していました。
- 数式:
usage_data/SERVICE_QUOTA(usage_data))*100
- メトリクス
-
usage_data
- 統計: 最大
- 名前空間:
AWS/Lambda
- 期間: 1分
- メトリクス名:
ConcurrentExecutions
-
SERVICE_QUOTA
関数はAWSドキュメントによると以下の説明が書かれており、特定の使用状況メトリクスを利用して、サービスクォータの使用率を監視+通知するCloudWatch Alarm
を作成することが可能となります。
特定の使用状況メトリクスのサービスクォータを返します。これを使用して、現在の使用状況とクォータの比較を可視化し、クォータに近づいたときに警告するアラームを設定できます。詳細については、「AWS 使用状況メトリクス」を参照してください。
ちなみに利用可能な使用状況メトリクスがこちらに書かれているのですが、2023年04月時点ではAWS Lambda
が含まれていない模様です。(英語版のドキュメントも書かれていないです)
ドキュメントの改訂が間に合っていないだけだと思いますが、もしここに書かれていないAWSサービスを監視したい場合は、Service Quotas
の各サービスクォータの詳細画面で使用率が利用できるか確認できそうなので、そちらを確認してみるのも良いと思います。
以下スクショは利用できない例ですが、Lambda関数の同時実行数はここにメトリクス値が表示されていました。(実施手順内のスクショ参照)
同じサービスクォータの申請が複数できないよう制御されている
自動化で懸念される一つとして、「CloudWatch Alarm
のアクションが大量に発生してしまった場合、AWSにも大量の上限緩和リクエストを送信してしまうのではないか」というものだと思っています。
そこで念のため、上限緩和リクエストを行った状態で、もう一度CloudWatch Alarm
が起動するようにLocust
を動かして動作検証を行ったところ、Lambda関数で以下のエラーが発生しました。
An error occurred (ResourceAlreadyExistsException) when calling the RequestServiceQuotaIncrease operation: Only one open service quota increase request is allowed per quota.
Service Quotas
のAPI側で多重リクエストできないような制御が含まれているようなので、無限リクエストは発生しない仕様になっていました。
仕様的にセーブされてはいるものの、CloudWatch Alarm
が大量に発生してしまうのはそもそも考慮不足ではありますので、万が一そのようなことが発生した場合には放置せずに閾値を見直しましょう。
ちなみに、Service Quotas
の画面からCloudWatch Alarm
を作成することができるのですが、その際には使用率を50%
以上を指定するように求められ、作成されたCloudWatch Alarm
の「アラームを実行するデータポイント」は10/10
が指定されるようでした。
おそらくその数値はAWS側が「これぐらいの閾値がよさそう」という暗黙の提案をされているのでは、という気がしますので、もし閾値に迷われる場合はこちらの値を参考にされるとよいかもしれません。
さいごに
今回はAWS Well-Architected Tool
を契機に、上限緩和申請の自動化を試みました。
今回の検証で、サービスクォータの使用率をCloudWatch
で知ることや、Service Quotas
のAPIで上限緩和申請が手軽に行えることなどが知れて良かったと思うと同時に、そのあたりのアップデート情報のキャッチアップが漏れていたことに少しショックを受けました。。。
AWSサービスの進化は目まぐるしいのでキャッチアップが漏れてしまうのは仕方ないかもしれませんが、そういった最新情報を拾うきっかけとして、定期的な設計・運用の見直しは行うべきなのだなと感じた次第です。
こちらからは以上です。