AWSでDockerイメージをLambda関数としてデプロイし、API Gatewayで公開する

ここのところ、生成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を使うことになるかもしれない自分への備忘録だったりするわけです・・・。

ご参考までに。

この記事を書いた人

井上 研一

株式会社ビビンコ代表取締役、ITエンジニア/経済産業省推進資格ITコーディネータ。AI・IoTに強いITコーディネータとして活動。画像認識モデルを活用したアプリや、生成AIを業務に組み込むためのサービス「Gen2Go」の開発などを行っている。近著に「使ってわかった AWSのAI」、「ワトソンで体感する人工知能」。日本全国でセミナー・研修講師としての登壇も多数。