Lambda Provisioned ConcurrencyのオートスケーリングをCDKで設定してみた
目次
はじめに
こんにちは、スカイアーチHRソリューションズのsugawaraです。
先日投稿した「LambdaのProvisioned Concurrencyを設定してみた」という記事では、コンソール画面からProvisioned Concurrency(プロビジョニング済み同時実行)を設定してみました。
今回はLambdaに対して、予測可能な負荷にしたがってスケーリングスケジュールを設定したいと思います。
なお、Lambdaのスケジュールされたスケーリングはコンソール画面からの操作は対応しておらず、現状はCLIで実行するようになっています。
実際に運用となった場合、CLIでのみの実行となると、設定の確認や変更が行いづらいと思います。
そのため、今回はCDKで設定したいと思います。
なぜProvisioned Concurrencyが必要かなどは、前回の記事を見ていただければと思います。
環境
今回利用する環境は下記になります。
- WSL2
- CDK 2.121.1
- Node.js 18.18.1
構築の流れ
シンプルなLambdaの作成
まずWSL上に作業フォルダscheduled_provisioned_concurrencyを作成し、そのディレクトリにてCDKの準備をします。
$ mkdir scheduled_provisioned_concurrency
$ cd scheduled_provisioned_concurrency
$ cdk init app --language typescript
作成されたフォルダは下記のような構成になっています。
├── bin
│ └── scheduled_provisioned_concurrency.ts // ここにenvを定義
├── cdk.json
├── jest.config.js
├── lib
│ └── scheduled_provisioned_concurrency.ts // ここにAWSリソースを定義
├── package.json
├── package-lock.json
├── README.md
├── test
│ └── scheduled_provisioned_concurrency.test.ts
└── tsconfig.json
bin/scheduled_provisioned_concurrency.tsファイルにて、デプロイするAWSアカウントとリージョンを指定します。今回はGithubなどにはpushしないため、シンプルにハードコーディングします。
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { ScheduledProvisionedConcurrencyStack } from '../lib/scheduled_provisioned_concurrency-stack';
const app = new cdk.App();
new ScheduledProvisionedConcurrencyStack(app, 'ScheduledProvisionedConcurrencyStack', {
env: {
account: 'YOUR_AWS_ACCOUNT',
region: 'ap-northeast-1'
}
});
なお、ハードコーディングしないデプロイ先の指定方法は下記の記事にまとめていますのでご参照ください。
まずはlib/scheduled_provisioned_concurrency.tsは下記のように定義してLambdaのみ作成します。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
export class ScheduledProvisionedConcurrencyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const lambdaFunction = new lambda.Function(this, 'LambdaFunction', {
functionName: "ScheduledProvisionedConcurrencyTestLambda",
description: "スケジュールされたプロビジョニング済みの検証用Lambda",
runtime: lambda.Runtime.PYTHON_3_12,
handler: 'lambda_function.lambda_handler',
code: lambda.Code.fromAsset(`../scheduled_provisioned_concurrency/lib`),
});
}
}
また、libフォルダ配下には下記のlambda_function.pyを作成します。
import json
def lambda_handler(event, context):
# TODO implement
return {
'statusCode': 200,
'body': json.dumps('Hello from Lambda!')
}
CDKコードが完成したら、scheduled_provisioned_concurrencyディレクトリにて下記のコマンドを実行します。
$ cdk diff
$ cdk deploy
すると、まだバージョンなどがないシンプルなLambdaが作成されます。
Provisioned Concurrencyの設定
次にProvisioned Concurrencyを設定していきます。前回の記事では、コンソール画面でバージョンの追加を行った上で、そのバージョンに対してProvisioned Concurrencyの設定を追加しました。この流れを先ほどのCDKのコードに追加していきます。
下記は今回のCDKコードの全体です。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as applicationautoscaling from 'aws-cdk-lib/aws-applicationautoscaling'
export class ScheduledProvisionedConcurrencyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const lambdaFunction = new lambda.Function(this, 'LambdaFunction', {
functionName: "ScheduledProvisionedConcurrencyTestLambda",
description: "スケジュールされたプロビジョニング済みの検証用Lambda",
runtime: lambda.Runtime.PYTHON_3_12,
handler: 'lambda_function.lambda_handler',
code: lambda.Code.fromAsset(`../scheduled_provisioned_concurrency/lib`),
});
// バージョンの設定
const lambdaVersion = new lambda.Version(this, 'LambdaVersion', {
lambda: lambdaFunction,
description: 'Version for Provisioned Concurrency',
});
// オートスケーリングのターゲットの設定
const scalingTarget = new applicationautoscaling.ScalableTarget(this, 'ScalableTarget', {
serviceNamespace: applicationautoscaling.ServiceNamespace.LAMBDA,
minCapacity: 3,
maxCapacity: 30,
resourceId: `function:${lambdaFunction.functionName}:${lambdaVersion.version}`,
scalableDimension: 'lambda:function:ProvisionedConcurrency',
});
// スケジュールされたスケーリングポリシーの追加
scalingTarget.scaleOnSchedule('ScaleOut', {
schedule: applicationautoscaling.Schedule.cron({ hour: '20', minute: '55' }), // JSTで5時55分
minCapacity: 10,
maxCapacity: 20,
});
scalingTarget.scaleOnSchedule('ScaleIn', {
schedule: applicationautoscaling.Schedule.cron({ hour: '21', minute: '5'}), // JSTで6時5分
minCapacity: 1,
maxCapacity: 2,
});
}
}
追加した部分について簡単に解説していきます。Provisioned Concurrencyを設定するために、まずはLambdaにバージョンを追加します。
// バージョンの設定
const lambdaVersion = new lambda.Version(this, 'LambdaVersion', {
lambda: lambdaFunction,
description: 'Version for Provisioned Concurrency',
});
これだけ追加してデプロイすると、下記のようにバージョン設定が追加されます。
次にProvisioned Concurrencyのオートスケーリングターゲットを設定します。今回は通常時はMinimumの3、上限としてMaxで30としています。
// オートスケーリングのターゲットの設定
const scalingTarget = new applicationautoscaling.ScalableTarget(this, 'ScalableTarget', {
serviceNamespace: applicationautoscaling.ServiceNamespace.LAMBDA,
minCapacity: 3,
maxCapacity: 30,
resourceId: `function:${lambdaFunction.functionName}:${lambdaVersion.version}`,
scalableDimension: 'lambda:function:ProvisionedConcurrency',
});
下記はスケールする前の状態です。ここからオートスケーリングさせていきます。
スケールアウト
Application Auto Scalingを用いてScheduled Provisioned Concurrencyでスケールアウトの設定をします。指定した時間になったらProvisioned Concurrencyは最小の10になるように記述します。
// スケジュールされたスケーリングポリシーの追加
scalingTarget.scaleOnSchedule('ScaleOut', {
schedule: applicationautoscaling.Schedule.cron({ hour: '20', minute: '55' }), // JSTで5時55分
minCapacity: 10,
maxCapacity: 20,
});
指定した時間になると自動でスケールアウトが始まります。
スケールアウトが完了すると、ステータスは準備完了になります。
スケールイン
次は指定の時間になったら自動でスケールインをするように設定を行います。通常時はMinimumの1で、上限はMaxで2とします。
scalingTarget.scaleOnSchedule('ScaleIn', {
schedule: applicationautoscaling.Schedule.cron({ hour: '21', minute: '5'}),
minCapacity: 1,
maxCapacity: 2,
});
指定した時間になると自動でスケールアウトが始まります。スケールインが完了すると、ステータスは準備完了になります。スケールアウトよりもすぐに変更が反映されます。
確認用CLIコマンド
最初に記載した通り、Lambdaのオートスケーリングはコンソール画面からの設定はできないようです。CDKで定義しても、本当に正しく設定されたかはコードだけではわかりません。
下記にスケジュールされたオートスケーリングについての確認用コマンドをまとめます。
まずはスケジュールされたオートスケーリングの設定についての確認コマンドとなります。CDKのコードで定義したものと見比べることができます。
aws application-autoscaling describe-scheduled-actions \
--service-namespace lambda \
--resource-id "function:ScheduledProvisionedConcurrencyTestLambda:1"
{
"ScheduledActions": [
{
"ScheduledActionName": "ScaleOut",
"ScheduledActionARN": "arn:aws:autoscaling:ap-northeast-1:008458347550:scheduledAction:f0304f65-5ebb-4349-a4c1-72d5bf206e2e:resource/lambda/function:ScheduledProvisionedConcurrencyTestLambda:1:scheduledActionName/ScaleOut",
"ServiceNamespace": "lambda",
"Schedule": "cron(55 20 * * ? *)",
"ResourceId": "function:ScheduledProvisionedConcurrencyTestLambda:1",
"ScalableDimension": "lambda:function:ProvisionedConcurrency",
"ScalableTargetAction": {
"MinCapacity": 10,
"MaxCapacity": 20
},
"CreationTime": "2024-04-29T05:31:26.318000+09:00"
},
{
"ScheduledActionName": "ScaleIn",
"ScheduledActionARN": "arn:aws:autoscaling:ap-northeast-1:008458347550:scheduledAction:f0304f65-5ebb-4349-a4c1-72d5bf206e2e:resource/lambda/function:ScheduledProvisionedConcurrencyTestLambda:1:scheduledActionName/ScaleIn",
"ServiceNamespace": "lambda",
"Schedule": "cron(5 21 * * ? *)",
"ResourceId": "function:ScheduledProvisionedConcurrencyTestLambda:1",
"ScalableDimension": "lambda:function:ProvisionedConcurrency",
"ScalableTargetAction": {
"MinCapacity": 1,
"MaxCapacity": 2
},
"CreationTime": "2024-04-29T05:31:26.217000+09:00"
}
]
}
次は実際に起こったオートスケーリングのアクティビティの一覧になります。いつどういった挙動が起こったのかが確認できます。
aws application-autoscaling describe-scaling-activities --service-namespace lambda \
--resource-id "function:ScheduledProvisionedConcurrencyTestLambda:1"
{
"ScalingActivities": [
{
"ActivityId": "80336cd1-be97-4102-bd88-82664610b037",
"ServiceNamespace": "lambda",
"ResourceId": "function:ScheduledProvisionedConcurrencyTestLambda:1",
"ScalableDimension": "lambda:function:ProvisionedConcurrency",
"Description": "Setting desired concurrency to 2.",
"Cause": "maximum capacity was set to 2",
"StartTime": "2024-04-30T06:05:08.716000+09:00",
"EndTime": "2024-04-30T06:05:44.816000+09:00",
"StatusCode": "Successful",
"StatusMessage": "Successfully set desired concurrency to 2. Change successfully fulfilled by lambda."
},
{
"ActivityId": "34227086-6cc0-4e6c-90ef-ec62a725828c",
"ServiceNamespace": "lambda",
"ResourceId": "function:ScheduledProvisionedConcurrencyTestLambda:1",
"ScalableDimension": "lambda:function:ProvisionedConcurrency",
"Description": "Setting min capacity to 1 and max capacity to 2",
"Cause": "scheduled action name ScaleIn was triggered",
"StartTime": "2024-04-30T06:05:08.357000+09:00",
"EndTime": "2024-04-30T06:05:08.381000+09:00",
"StatusCode": "Successful",
"StatusMessage": "Successfully set min capacity to 1 and max capacity to 2"
},
{
"ActivityId": "495d638c-f5b8-4bc4-a549-efabbdf6a5e5",
"ServiceNamespace": "lambda",
"ResourceId": "function:ScheduledProvisionedConcurrencyTestLambda:1",
"ScalableDimension": "lambda:function:ProvisionedConcurrency",
"Description": "Setting desired concurrency to 10.",
"Cause": "minimum capacity was set to 10",
"StartTime": "2024-04-30T05:55:33.538000+09:00",
"EndTime": "2024-04-30T05:57:15.176000+09:00",
"StatusCode": "Successful",
"StatusMessage": "Successfully set desired concurrency to 10. Change successfully fulfilled by lambda."
},
{
"ActivityId": "1572a5c0-9524-41df-8faa-2ea81750e9ab",
"ServiceNamespace": "lambda",
"ResourceId": "function:ScheduledProvisionedConcurrencyTestLambda:1",
"ScalableDimension": "lambda:function:ProvisionedConcurrency",
"Description": "Setting min capacity to 10 and max capacity to 20",
"Cause": "scheduled action name ScaleOut was triggered",
"StartTime": "2024-04-30T05:55:33.183000+09:00",
"EndTime": "2024-04-30T05:55:33.192000+09:00",
"StatusCode": "Successful",
"StatusMessage": "Successfully set min capacity to 10 and max capacity to 20"
},
{
"ActivityId": "ac3dc6d3-8d09-4691-9d55-f9fa4a871f97",
"ServiceNamespace": "lambda",
"ResourceId": "function:ScheduledProvisionedConcurrencyTestLambda:1",
"ScalableDimension": "lambda:function:ProvisionedConcurrency",
"Description": "Setting min capacity to 10 and max capacity to 20",
"Cause": "scheduled action name ScaleOut was triggered",
"StartTime": "2024-04-30T05:45:08.611000+09:00",
"EndTime": "2024-04-30T05:45:08.633000+09:00",
"StatusCode": "Successful",
"StatusMessage": "Successfully set min capacity to 10 and max capacity to 20"
},
...
]
}
最後に、AWS Application Auto Scalingサービスに登録されているスケーラブルターゲットの設定と状態を確認するコマンドです。最後のスケールインポリシーが表示されています。
{
"ScalableTargets": [
{
"ServiceNamespace": "lambda",
"ResourceId": "function:ScheduledProvisionedConcurrencyTestLambda:1",
"ScalableDimension": "lambda:function:ProvisionedConcurrency",
"MinCapacity": 1,
"MaxCapacity": 2,
"RoleARN": "arn:aws:iam::008458347550:role/aws-service-role/lambda.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_LambdaConcurrency",
"CreationTime": "2024-04-29T05:31:26.121000+09:00",
"SuspendedState": {
"DynamicScalingInSuspended": false,
"DynamicScalingOutSuspended": false,
"ScheduledScalingSuspended": false
},
"ScalableTargetARN": "arn:aws:application-autoscaling:ap-northeast-1:008458347550:scalable-target/07mdf0304f655ebb4349a4c172d5bf206e2e"
}
]
}
おわりに
今回はLambdaのProvisioned Concurrencyにオートスケーリングの設定をCDKで定義してみました。
スケジュールされたProvisioned ConcurrencyをCDKで設定する場合、LambdaFunctionだけでなく、LambdaVersionやApplicationAutoScalingも記述する必要があります。そのため、予想されるスパイクに対応するために細かく制御したい場合、スケジュールされたオートスケーリングは不向きかもしれません。メトリクスによるターゲット追跡のオートスケーリングの方がいいかもしれません。今後そちらも検証していきたいと思います。