ここのところ、生成AIを活用したあるサービスを開発していて(株式会社ビビンコとして初のサブスクサービスになる予定!)、サービスとして提供するAPIをDockerイメージとして開発し、それをLambda関数としてデプロイ、さらにAPI Gatewayを使って公開ということを画策しているわけです。(それとは別にダッシュボード相当のWebアプリがある。)
ここ最近、AWSで何かするときはCDKを使いたい体になっているので、このようなcdk-stack.tsを作りました。
import * as cdk from 'aws-cdk-lib'
import { Construct } from 'constructs'
import * as lambda from 'aws-cdk-lib/aws-lambda'
import * as ecr_assets from 'aws-cdk-lib/aws-ecr-assets'
import * as apigateway from 'aws-cdk-lib/aws-apigateway'
import * as ssm from 'aws-cdk-lib/aws-ssm'
import * as path from 'path'
export class CdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props)
// DockerイメージのビルドとECRへのプッシュ
const dockerImageAsset = new ecr_assets.DockerImageAsset(this, 'Gen2GoApiImage',
{
directory: path.join('..', 'app')
}
)
// Lambda関数の作成
const lambdaFunction = new lambda.DockerImageFunction(this, 'Gen2GoApiFunction', {
code: lambda.DockerImageCode.fromEcr(dockerImageAsset.repository, {
tagOrDigest: dockerImageAsset.assetHash
}),
functionName: 'Gen2GoApi',
timeout: cdk.Duration.seconds(300)
})
// APIの作成
const api = new apigateway.RestApi(this, 'Gen2GoApi', {
restApiName: 'Gen2GoAPI',
description: 'REST API for Gen2Go Lambda Function'
})
// Lambda統合の作成
const lambdaIntegration = new apigateway.LambdaIntegration(lambdaFunction, {
requestTemplates: { 'application/json': '{ "StatusCode": "200" }' },
})
// プロキシリソースの作成とLambda統合の連携
const proxyResource = api.root.addResource('{proxy+}')
proxyResource.addMethod('ANY', lambdaIntegration, {
apiKeyRequired: true
})
// 管理用APIキーの作成
const adminApiKey = new apigateway.ApiKey(this, 'Gen2GoAdminApiKey', {
apiKeyName: 'Gen2GoAdminKey'
})
// 管理用使用量プランの作成
const adminUsagePlan = new apigateway.UsagePlan(this, 'Gen2GoAdminUsagePlan', {
name: 'Gen2Go-AdminUsagePlan',
})
adminUsagePlan.addApiKey(adminApiKey)
adminUsagePlan.addApiStage({
api: api,
stage: api.deploymentStage
})
// 無償使用量プランの作成
const freeUsagePlan = new apigateway.UsagePlan(this, 'Gen2GoFreeUsagePlan', {
name: 'Gen2Go-FreeUsagePlan',
throttle: {
rateLimit: 1,
burstLimit: 2,
},
quota: {
limit: 20,
period: apigateway.Period.MONTH
}
})
freeUsagePlan.addApiStage({
api: api,
stage: api.deploymentStage
})
// 有償使用量プランの作成
const paidUsagePlan = new apigateway.UsagePlan(this, 'Gen2GoPaidUsagePlan', {
name: 'Gen2Go-PaidUsagePlan',
throttle: {
rateLimit: 10,
burstLimit: 20,
}
})
paidUsagePlan.addApiStage({
api: api,
stage: api.deploymentStage
})
// 出力値の作成
const configValues = {
api_endpoint: api.url,
api_id: api.restApiId,
api_deployment_stage: api.deploymentStage.stageName,
admin_usage_plan_id: adminUsagePlan.usagePlanId,
admin_api_key_id: adminApiKey.keyId,
free_usage_plan_id: freeUsagePlan.usagePlanId,
paid_usage_plan_id: paidUsagePlan.usagePlanId,
}
// SSMへのセット
for (const [key, value] of Object.entries(configValues)) {
new ssm.StringParameter(this, `Gen2GoParam_${key}`, {
parameterName: `/gen2go/prod/${key}`,
stringValue: value
})
}
// 出力の作成
new cdk.CfnOutput(this, 'ConfigValues', {
value: JSON.stringify(configValues)
})
}
}
Gen2Goという謎のキーワードが出とるやないかい!というツッコミも入りそうですが、まぁ、良いです。
Lambda関数としてデプロイするAPIアプリはPythonでFastAPIを使って開発しています。だけど、CDKはTypeScriptです。この辺は開発している言語と、CDKで使う言語は関係ないので問題ありません。
そのFastAPIのアプリは、../app
に入っていて、Dockerfileもそこにあるので、その内容をDockerイメージとしてビルドしてECRにプッシュしています。
あとは、そのDockerイメージをLambda関数としてデプロイ→API GatewayのLambda統合の設定という流れです。
API Gatewayで使える使用量プランの設定とか、APIキーの発行とかもやっています。いいですね。
で、それをあとで何かに使うためにSSMに格納したりしています。
と、これはこれとしてしっかり使えるCDKスタックになっていると思うので、ひとまずここに書いておこうというわけですが、結局、これは使わないことにしました。
なぜって、API Gatewayは29秒でタイムアウトになるという落とし穴があることをすっかり忘れていたからです。
生成AIを活用するサービスなので、29秒で生成が終わらないことはざらにあります。
WebSocketを使うサービスにすればAPI Gatewayのままでいけるとか、そういう話もあるし、当然にWebSocketなりを使ったストリーミング型のレスポンスを返すようにしないといけないとも思っているのですが、かといって同期型のAPIがまったくいらないかというと、そうでもなく。
ということで、Lambda関数をALB(Applecation Load Balancer)に直接つなぐという技で行こうかと考えているのであります。
なので、この記事は、いつかAPI Gatewayを使うことになるかもしれない自分への備忘録だったりするわけです・・・。
ご参考までに。