Serverless FrameworkでLambda Provisioned ConcurrencyのAutoScalingを実装する

2023.03.16

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

今回はLambdaのProvisioned Concurrencyを指定された時間帯のみスケールアウトする環境をServerless Frameworkを使って実装する方法についてご相談いただきましたので、クラスメソッドらしくブログで回答したいと思います。

Provisioned Concurrency(おさらい)

Provisioned ConcurrencyはLambda関数の実行環境を予め待機させておくことで、コールドスタートによるスタートアップ時間を短縮し、処理時間の短縮や応答性の向上を実現する機能です。

実行時間あたりの料金は通常の料金よりも安く設定されていますが、一方で待機時間でも料金が発生する仕組みとなっています。

背景(前提)

  • 特定の時間帯にリクエストが集中するサービスを提供している
  • 待機時間コスト削減のためProvisioned Concurrencyを特定時間にのみスケールアウトさせたい
  • Serverless Frameworkで実装したい

Provisioned ConcurrencyをAutoScalingさせる

以下がProvisioned Concurrencyを特定時間にスケールアウト、スケールインする環境のserverless.ymlです。

  • 毎日15:00(JST)になるとscale-outアクションが発動し、Provisioned Concurrencyが10になる
  • 毎日15:05(JST)になるとscale-inアクションが発動し、Provisioned Concurrencyが0になる
service: as-provisionedconcurrency-sls

frameworkVersion: '3'

provider:
  name: aws
  runtime: python3.8
  region: ap-northeast-1
  stage: ${sls:stage}

functions:
  hello:
    handler: handler.hello
    provisionedConcurrency: 1

resources:
  Resources:
    ScheduledConcurrency:
      Type: AWS::ApplicationAutoScaling::ScalableTarget
      Properties:
        MaxCapacity: 10
        MinCapacity: 0
        ResourceId: function:${self:service}-${self:provider.stage}-hello:provisioned
        RoleARN: arn:aws:iam::${aws:accountId}:role/aws-service-role/lambda.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_LambdaConcurrency
        ScalableDimension: lambda:function:ProvisionedConcurrency
        ScheduledActions: 
          - ScalableTargetAction:
              MaxCapacity: 10
              MinCapacity: 10
            Schedule: 'cron(0 6 * * ? *)'
            ScheduledActionName: scale-out
          - ScalableTargetAction:
              MaxCapacity: 0
              MinCapacity: 0
            Schedule: 'cron(5 6 * * ? *)'
            ScheduledActionName: scale-in 
        ServiceNamespace: lambda
      DependsOn: HelloProvConcLambdaAlias

以下のコマンドでデプロイします。(ステージを指定しない場合、デフォルトはdevです)

serverless deploy --stage dev

以降はserverless.yml内でポイントとなる点を取り上げて説明します。

provisionedConcurrency を指定する

functions:
  hello:
    handler: handler.hello
    provisionedConcurrency: 1

Provisioned Concurrencyを設定するにはfunctions:内でprovisionedConcurrency:を設定します。

後続で説明しますがAWS::ApplicationAutoScaling::ScalableTargetを設定する際にLambda関数のバージョン、またはエイリアスの指定が必要になりますが$LATESTは指定できません。そのため何らかのエイリアス作成が必要になりますがprovisionedConcurrency:を指定することでprovisionedというエイリアスが自動作成してくれます。

今回はhello:を指定していますので、この場合に作成されるAWS::Lambda::Aliasの論理IDはHelloProvConcLambdaAliasとなります。

(余談)自前でAlias定義するのを諦めた話

注意点としては、provisionedConcurrency: 1を指定しているため初回デプロイ〜初回のスケジュールアクションが発動するまではProvisioned Concurrencyが1の状態が維持されますので待機コストが掛かります。provisionedConcurrency: 0としてエイリアスだけ作成されることを期待しましたが、0を指定した場合はエイリアスは作成されませんでした。

それならばresources:内でAWS::Lambda::Aliasを自前で定義するか、、と試みましたがエイリアス定義に必要となるAWS::Lambda::Version論理IDにダイジェスト値(ハッシュ値)が含まれるようで、更にこのハッシュ値をserverless.yml内で取得する方法がよく判らない、、ということで私は諦めました。。

Application Auto Scalingの併用

Provisioned ConcurrencyのAutoScalingは、Application Auto Scalingを併用して実装します。

resources:
  Resources:
    ScheduledConcurrency:
      Type: AWS::ApplicationAutoScaling::ScalableTarget
      Properties:
        MaxCapacity: 10
        MinCapacity: 0
        ResourceId: function:${self:service}-${self:provider.stage}-hello:provisioned
        RoleARN: arn:aws:iam::${aws:accountId}:role/aws-service-role/lambda.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_LambdaConcurrency
        ScalableDimension: lambda:function:ProvisionedConcurrency

先述のとおりResourceId:の指定にLambda関数名+バージョンまたはエイリアスの指定が必要です。provisionedConcurrency:によって自動生成されたprovisionedをエイリアス名に指定します。

Application Auto Scalingを設定するためのIAMロールが必要となりますので、RoleARN:でサービスリンクされたIAMロールを指定します。存在しない場合でも、デフォルトで決まっているサービスリンクされたRoleARNの指定すれば自動的に作成されます。

明示的に依存関係を指定する

DependsOn: HelloProvConcLambdaAlias

依存関係としてエイリアスの論理IDを明示的に指定する必要があります。指定していない場合、以下のようなエラーになりました。

Error:
CREATE_FAILED: ScheduledConcurrency (AWS::ApplicationAutoScaling::ScalableTarget)
Validation failed for resource: function:as-provisionedconcurrency-sls-dev-hello:provisioned, scalable dimension: lambda:function:ProvisionedConcurrency. Reason: Cannot find alias arn: arn:aws:lambda:ap-northeast-1:123456789012:function:as-provisionedconcurrency-sls-dev-hello:provisioned (Service: AWSApplicationAutoScaling; Status Code: 400; Error Code: ValidationException; Request ID: abd268ba-ee09-4fb8-b4a5-57313c5460fe; Proxy: null)

動作確認

初回デプロイ後〜初回スケジュールアクションまでの間、先述のとおりProvisioned Concurrencyが1の状態になります。

スケジュールによってscale-outが発動し、しばらく待つとProvisioned Concurrencyが10になりました。

スケジュールによってscale-inが発動し、しばらく待つとProvisioned Concurrencyが0になりました。

以下、スケーリングアクティビティのログです。

$ aws application-autoscaling describe-scaling-activities --service-namespace lambda
ScalingActivities:
- ActivityId: afd024c2-5563-4dcf-a2a0-05be7e60b125
  Cause: maximum capacity was set to 0
  Description: Setting desired concurrency to 0.
  EndTime: '2023-03-16T16:35:50.939000+09:00'
  ResourceId: function:as-provisionedconcurrency-sls-dev-hello:provisioned
  ScalableDimension: lambda:function:ProvisionedConcurrency
  ServiceNamespace: lambda
  StartTime: '2023-03-16T16:35:16.628000+09:00'
  StatusCode: Successful
  StatusMessage: Successfully set desired concurrency to 0. Change successfully fulfilled
    by lambda.
- ActivityId: 2953232c-1769-4cd4-8cc7-b5a3e90a2ee9
  Cause: scheduled action name scale-in was triggered
  Description: Setting min capacity to 0 and max capacity to 0
  EndTime: '2023-03-16T16:35:16.301000+09:00'
  ResourceId: function:as-provisionedconcurrency-sls-dev-hello:provisioned
  ScalableDimension: lambda:function:ProvisionedConcurrency
  ServiceNamespace: lambda
  StartTime: '2023-03-16T16:35:16.292000+09:00'
  StatusCode: Successful
  StatusMessage: Successfully set min capacity to 0 and max capacity to 0
- ActivityId: 8ff236d7-c52e-4707-9e92-352a62ff19e4
  Cause: minimum capacity was set to 10
  Description: Setting desired concurrency to 10.
  EndTime: '2023-03-16T16:32:51.738000+09:00'
  ResourceId: function:as-provisionedconcurrency-sls-dev-hello:provisioned
  ScalableDimension: lambda:function:ProvisionedConcurrency
  ServiceNamespace: lambda
  StartTime: '2023-03-16T16:30:43.762000+09:00'
  StatusCode: Successful
  StatusMessage: Successfully set desired concurrency to 10. Change successfully fulfilled
    by lambda.
- ActivityId: 59828e6a-51da-44f0-8427-fe892ed998a3
  Cause: scheduled action name scale-out was triggered
  Description: Setting min capacity to 10 and max capacity to 10
  EndTime: '2023-03-16T16:30:43.421000+09:00'
  ResourceId: function:as-provisionedconcurrency-sls-dev-hello:provisioned
  ScalableDimension: lambda:function:ProvisionedConcurrency
  ServiceNamespace: lambda
  StartTime: '2023-03-16T16:30:43.396000+09:00'
  StatusCode: Successful
  StatusMessage: Successfully set min capacity to 10 and max capacity to 10
(省略)

まとめ

Provisioned Concurrencyはコールドスタートによる遅延を回避するために有効な手段ですが、ピーク時とピークアウト時のギャップが大きな場合、無駄な待機コストが発生する可能性があります。Application Auto Scalingを併用することで負荷やスケジュールによって自動的にスケールアウト/インが可能ですので、巧く利用して無駄なコストを削減してみてはいかがでしょうか。

参考情報