🔨

[CDK] Lambdaを介さずAPIGateway+SNS連携する構成

2022/07/03に公開2

APIを作るとき、まずAPIGW→Lambdaを検討してしまいがちですが、下記の場合などはAPI処理の非同期化を検討することになります。

最も、チャットボットの方は下記の方法で対処できる可能性があるようです(今調べていてこのようなテクニックがあることを知りました)。

Slack Bot をサーバーレスで運用する時の、タイムアウト対策【小技】 - Qiita

今回はAPIGatewayにAmazon SNSを組み合わせて非同期化する構成を試してみたので紹介します。構築にCDKを使います。

構成図

APIGatewayがリクエストを受けるとSNSにPublishを行いレスポンスします。これによりLambdaの処理を待たないようになります。

環境

  • typescript 4.7.4
  • esbuild 0.14.48
  • aws-cdk 2.30.0
  • aws-cdk-lib 2.30.0
  • constructs 10.1.43

セットアップ

CDKコード

下記のCDKコードを用意し、デプロイします。

lib/apigateway-stack.ts
import { Construct } from "constructs"
import * as cdk from "aws-cdk-lib"

export class ApigatewayStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    // Lambda関数
    const lambdaFn = new cdk.aws_lambda_nodejs.NodejsFunction(this, "fn", {
      runtime: cdk.aws_lambda.Runtime.NODEJS_16_X,
      entry: "lambda-handler/from-sns-handler.ts",
    })

    // SNSトピック
    const topic = new cdk.aws_sns.Topic(this, "topic", {})
    topic.addSubscription(
      new cdk.aws_sns_subscriptions.LambdaSubscription(lambdaFn)
    )

    // APIGW用IAMロール
    const apiRole = new cdk.aws_iam.Role(this, "apiRole", {
      assumedBy: new cdk.aws_iam.ServicePrincipal("apigateway.amazonaws.com"),
    })
    topic.grantPublish(apiRole)

    // APIGW
    const api = new cdk.aws_apigateway.RestApi(this, "api", {
      cloudWatchRole: false,
    })
    api.root.addMethod(
      "POST",
      new cdk.aws_apigateway.AwsIntegration({
        service: "sns",
        region: "ap-northeast-1",
        integrationHttpMethod: "POST",
        action: "Publish",
        options: {
          credentialsRole: apiRole,
          requestParameters: {
            "integration.request.querystring.Message": "method.request.body",
            "integration.request.querystring.TopicArn": `'${topic.topicArn}'`,
          },
          integrationResponses: [
            {
              statusCode: "200",
	      // SNSのレスポンスをそのまま返したい場合(パススルー)は記述しなくて良い
              responseTemplates: {
                "application/json": '{ message: "Success." }',
              },
            },
            {
              statusCode: "400",
              selectionPattern: "4\\d{2}",
              responseTemplates: {
                "application/json": '{ message: "Invalid request." }',
              },
            },
            {
              statusCode: "500",
              selectionPattern: "5\\d{2}",
              responseTemplates: {
                "application/json": '{ message: "Internal server error." }',
              },
            },
          ],
        },
      }),
      {
        methodResponses: [
          {
            statusCode: "200",
          },
          {
            statusCode: "400",
          },
          {
            statusCode: "500",
          },
        ],
      }
    )
  }
}
bin/aws-playground.ts
#!/usr/bin/env node
import "source-map-support/register"
import * as cdk from "aws-cdk-lib"
import { ApigatewayStack } from "../lib/apigateway-stack"

const app = new cdk.App()
new ApigatewayStack(app, "ApigatewayStack", {
  env: { region: "ap-northeast-1" },
})

Lambda関数コード

イベントを出力するだけのシンプルなLambda関数です。

lambda-handler/from-sns-handler.ts
import { SNSEvent } from "aws-lambda"

export const handler = async (event: SNSEvent): Promise<void> => {
  console.log(JSON.stringify({ event }))
}

動作確認する

テストリクエストを送信

curl -X POST \
-H "Content-Type: application/json" \
-d '{"message": "Hello apigw+sns!"}' https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/
# => { message: "Success." }

CloudWatch Logsを確認

2022-07-02T11:29:35.076Z	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx	INFO
{
    "event": {
        "Records": [
            {
                "EventSource": "aws:sns",
                "EventVersion": "1.0",
                "EventSubscriptionArn": "arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:xxxxxxxxxxxxxxxxxxx:xxxxxxxxxxxxxxxxxx",
                "Sns": {
                    "Type": "Notification",
                    "MessageId": "xxxxxxxxxxxxxxxxxx",
                    "TopicArn": "arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxx",
                    "Subject": null,
                    "Message": "{\"message\":\"Hello apigw+sns!\"}",
                    "Timestamp": "2022-07-02T11:29:34.694Z",
                    "SignatureVersion": "1",
                    "Signature": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
                    "SigningCertUrl": "https://sns.ap-northeast-1.amazonaws.com/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
                    "UnsubscribeUrl": "https://sns.ap-northeast-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
                    "MessageAttributes": {}
                }
            }
        ]
    }
}

"Message": "{\"message\":\"Hello apigw+sns!\"}" が来ていることが確認できました。

参考

Discussion

waddy_uwaddy_u

こんなやり方があるのですね!参考になりました。ちなみに、API Gateway から Lambda を呼ぶときに非同期起動モードもあるのですが、こちらでは要件を満たせない事情がありましたか?

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/set-up-lambda-integration-async.html

dyoshikawadyoshikawa

コメントありがとうございます。気づくのが遅れました。

API Gateway から Lambda を呼ぶときに非同期起動モードもある

なるほど、この機能を知らなかったです。
確実に選択肢に入ってくる機能に思われるので、機会があれば検証したいと思います。情報ありがとうございます。