API Gateway(HTTP API)+Lambdaの構成にする時にProvisioned Concurrencyを有効にしてコールドスタート対策を行うケースがあると思います。コストは通常のLambdaに比べて大幅に増加してしまいますが、それでもパフォーマンス面で費用対効果にメリットがある場合は有効なオプションです。
CDKでこれらを設定する場合に、単純にLambdaのオプションを有効化すれば良いというだけではありません。
LambdaのAliasを作成してそこに対してAPI GatewayのインテグレーションやIAMポリシーの設定をしたり、必要に応じてAuto Scalingの設定をしたりと少し煩雑です。
この記事ではそれらの実装方法を備忘録として書いています。
CDKの実装のポイント
まず、NodejsFunction
コンストラクタで作成したLambdaにAliasを有効化します。名前は分かりやすくProvisioned
とかで良いかと思います。
const apiBackedFunctionAlias = apiBackedFunction.currentVersion.addAlias('provisioned')
そして以下の通りAutoScalingの設定をこのAliasに行います。以下の通り設定すると最低のLambdaインスタンス数が5、最大が30そしてインスタンス数の7割を消費すると新たなインスタンスが起動する設定になっています。
apiBackedFunctionAlias.addAutoScaling(
{
maxCapacity: 30,
minCapacity: 5,
}
).scaleOnUtilization({
utilizationTarget: 0.7
})
API GatewayからのIAM設定もLambdaファンクション自体ではなくてAliasに対して行うことを注意して下さい。
apiBackedFunctionAlias.addPermission('testFunctionPermission', {
principal: new iam.ServicePrincipal('apigateway.amazonaws.com'),
action: 'lambda:InvokeFunction',
sourceArn: `arn:aws:execute-api:${cdk.Stack.of(this).region}:${cdk.Stack.of(this).account}:${api.ref}/*/*/*`,
})
CDKの実装全体
以下が実装の全体です。
import * as cdk from '@aws-cdk/core'
import { Runtime, Tracing } from '@aws-cdk/aws-lambda'
import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs'
import { CfnApi, CfnStage } from '@aws-cdk/aws-apigatewayv2'
import * as iam from '@aws-cdk/aws-iam'
export interface ApiProps extends cdk.StackProps {
stage: string
}
export class ApiStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props: ApiProps) {
super(scope, id, props)
const apiBackedexecutionLambdaRole = new iam.Role(this, `testApiBackedFunctionRole-${props.stage}`, {
roleName: `apiBackedFunctionRole-${props.stage}`,
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
]
})
const apiBackedFunction = new NodejsFunction(this, 'testApiBackedFunction', {
entry: 'src/api.ts',
handler: 'handler',
runtime: Runtime.NODEJS_14_X,
timeout: cdk.Duration.seconds(30),
memorySize: 1024,
bundling: {
forceDockerBundling: false,
sourceMap: true
},
role: apiBackedexecutionLambdaRole,
})
const apiBackedFunctionAlias = apiBackedFunction.currentVersion.addAlias('provisioned')
apiBackedFunctionAlias.addAutoScaling(
{
maxCapacity: 30,
minCapacity: 5,
}
).scaleOnUtilization({
utilizationTarget: 0.7
})
const integrationSetting = {
type: 'AWS_PROXY',
httpMethod: 'POST',
uri: apiBackedFunctionAlias.functionArn,
payloadFormatVersion: '1.0',
}
const api = new CfnApi(this, 'httpApi')
const apiRole = new iam.Role(this, 'RestApiAuthHandlerRole', {
assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
})
apiBackedFunctionAlias.grantInvoke(apiRole)
const policyStatement = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [ 'sts:AssumeRole' ],
resources: [ '*' ],
})
const assumePolicy = new iam.Policy(this, 'StsAssumeForApigateway')
assumePolicy.addStatements(policyStatement)
apiRole.attachInlinePolicy(assumePolicy)
new CfnStage(this, `Bousai-${props.stage}`, {
apiId: api.ref,
stageName: '$default',
autoDeploy: true,
})
apiBackedFunctionAlias.addPermission('testFunctionPermission', {
principal: new iam.ServicePrincipal('apigateway.amazonaws.com'),
action: 'lambda:InvokeFunction',
sourceArn: `arn:aws:execute-api:${cdk.Stack.of(this).region}:${cdk.Stack.of(this).account}:${api.ref}/*/*/*`,
})
}
}
動作確認
CDKでのデプロイが完了するとキャプチャの通り、指定した数だけLambdaがプロビジョニングされています。AutoScalingの設定が有効になっているかはマネンジメントコンソールからは分かりませんが、実際にAPiに負荷をかけると発動することが分かります。