1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

AWS Service QuotasのAPIを利用してLambda関数の同時実行数の上限緩和申請を自動化してみた。

Last updated at Posted at 2023-04-29

はじめに

最近、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リソースを準備します。
aws.drawio.png

処理シーケンス

上記構成図の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

実施手順

  1. 上限緩和申請に必要な情報を事前に確認
    Service Quotasの管理画面から以下のカテゴリの「サービスコード」と「クォーターコード」を確認しておきます。
    • AWSのサービス:AWS Lambda
    • サービスクォータ:Concurrent executions(同時実行数)
      image.png
      上記情報は後ほどService QuotasのAPIを利用する際に利用します。
  2. 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.py
      import 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
      
  3. 上限緩和自動化処理構築(スタック名:ServiceQuotaStack
    1.にて確認した「サービスコード」と「クォーターコード」をFUNCTION_ENV_SERVICE_CODEFUNCTION_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.py
      import 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
      
  4. 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("")
      
      Dockerfile
      FROM locustio/locust
      WORKDIR /mnt/locust
      COPY *.py /mnt/locust/
      
      docker-compose.yml
      version: '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にて負荷開始!(スクショのみ)
      image.png
      image.png
  5. マネージメントコンソールから想定通り動いているか確認
    まずはService Quotasの画面を確認。
    4.で実施したタイミングで使用率が10%を超えている、かつクォータ増加リクエストが想定通りの値(現在の上限値50+5=55)で申請されていることが確認できました。
    image.png
    image.png
    念のためCloudWatch Alarmの画面も確認すると、Service Quitasのメトリクスと同時刻(2023-04-29 04:30ごろ)にSNSへのアクションが実行されたことも確認できました。
    image.png
  6. さらに時間経過でService Quotasの画面を確認
    記事をまとめている間(3時間程度経過後?)にもう一度Service Quotasの画面を確認したところ、適用されたクォータ値が50から55に更新されていることも確認できました。
    image.png
    AWSサポート画面でも「対応したよ」というメッセージが確認できました。
    image.png
    これで検証は終了です。
    ちなみに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が含まれていない模様です。(英語版のドキュメントも書かれていないです)
image.png
ドキュメントの改訂が間に合っていないだけだと思いますが、もしここに書かれていないAWSサービスを監視したい場合は、Service Quotasの各サービスクォータの詳細画面で使用率が利用できるか確認できそうなので、そちらを確認してみるのも良いと思います。
以下スクショは利用できない例ですが、Lambda関数の同時実行数はここにメトリクス値が表示されていました。(実施手順内のスクショ参照)
image.png

同じサービスクォータの申請が複数できないよう制御されている

自動化で懸念される一つとして、「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側が「これぐらいの閾値がよさそう」という暗黙の提案をされているのでは、という気がしますので、もし閾値に迷われる場合はこちらの値を参考にされるとよいかもしれません。
image.png

さいごに

今回はAWS Well-Architected Toolを契機に、上限緩和申請の自動化を試みました。
今回の検証で、サービスクォータの使用率をCloudWatchで知ることや、Service QuotasのAPIで上限緩和申請が手軽に行えることなどが知れて良かったと思うと同時に、そのあたりのアップデート情報のキャッチアップが漏れていたことに少しショックを受けました。。。
AWSサービスの進化は目まぐるしいのでキャッチアップが漏れてしまうのは仕方ないかもしれませんが、そういった最新情報を拾うきっかけとして、定期的な設計・運用の見直しは行うべきなのだなと感じた次第です。

こちらからは以上です。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?