🍙

Amazon Simple Notification ServiceからAWS Lambdaにメッセージを受け渡す with Scala

2025/02/04に公開

はじめに

AWSでシステムを構築する際、各サービスの連携は重要な要素の1つです。
各サービス間の非同期な処理を実現するためには、メッセージングサービスを利用することが多いのではないでしょうか。
本記事では、メッセージングサービスである、Amazon Simple Notification Serviceをトリガーとして、AWS Lambda(以降、Lambdaと表記します)を実行する仕組みを実装します。

Amazon Simple Notification Serviceとは

Amazon Simple Notification Service(以降、SNSと表記します)は、サービス間でメッセージを送受信するための「ハブ」のような役割を果たします。
具体的には、あるアプリケーションがSNSにメッセージを送信すると、SNSは受け取ったメッセージをAmazon SQS、Lambda、モバイルテキストメッセージ (SMS) など、さまざまな配信先に送信できます。
本記事では、AWS CLIからSNSを呼び出し、Lambdaにメッセージを受け渡します。

引用: Amazon SNS とは
引用: 【初心者でも5分で分かる】Amazon SNS

実装

動作確認環境

  • OS: macOS Ventura 13.6(Apple M1チップ搭載)
  • AWS CLI: 2.8.5
  • Scala: 2.13.12
  • Java: 11

1. Lambda用のデプロイパッケージを作成

AWS Lambda function in Scala with Container Imageを参考に、Lambda関数をデプロイするためのパッケージを作成します。

事前準備

project/plugins.sbtに以下の設定を追加します。
以下の記述を追加することで、sbt-assemblyプラグインをプロジェクトに追加します。
このプラグインは、複数のJARファイルを1つにまとめたFAT JARを作成するために使用します。

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")

build.sbt ファイルに以下の設定を追記します。
これは、LambdaでJava(JVM言語)を実行する際に必要なライブラリと、sbt-assemblyプラグインの設定です。

libraryDependencies += "com.amazonaws" % "aws-lambda-java-core" % "1.1.0"
libraryDependencies += "com.amazonaws" % "aws-lambda-java-events" % "1.1.0"

assembly / assemblyOutputPath := file("target/function.jar")
assembly / assemblyMergeStrategy := {
  case PathList("module-info.class") => MergeStrategy.last
  case path if path.endsWith("/module-info.class") => MergeStrategy.last
  case x =>
    val oldStrategy = (assembly / assemblyMergeStrategy).value
    oldStrategy(x)
}
  • com.amazonaws % aws-lambda-java-core: Lambda関数を実行するために必要なライブラリです

  • com.amazonaws % aws-lambda-java-events: Lambda関数でイベントを処理するために必要なライブラリです。今回は、SNSイベントを扱うために使用します

  • assembly / assemblyOutputPath := file("target/function.jar"): sbt-assemblyプラグインで作成するFAT JARファイルの出力先をtarget/function.jarに設定します

  • assembly / assemblyMergeStrategy := { ... }: sbt-assemblyプラグインで複数のJARファイルを結合する際、module-info.classが重複する問題を解決するための設定です
    この設定は、sbt-assembly で FAT JAR を生成しようとしたら module-info.class の重複で怒られた話を参考にしています。

1.1 実行するコードの作成

Lambda関数として実行する、Scalaのコードを作成します。
このコードは、SNSから受け取ったメッセージをログに出力する処理を行います。

基本的にAmazon SNS トリガーから Lambda 関数を呼び出すで紹介されていた、JavaのコードをScalaで置き換えたものとなります。

package com.example

import scala.util.{ Failure, Success, Try }
import scala.jdk.CollectionConverters._

import com.amazonaws.services.lambda.runtime.{ Context, LambdaLogger, RequestHandler }
import com.amazonaws.services.lambda.runtime.events.SNSEvent
import com.amazonaws.services.lambda.runtime.events.SNSEvent.SNSRecord


class LambdaHandler() extends RequestHandler[SNSEvent, Boolean] {

  private def getSnsMessage(record: SNSRecord): Try[String] = Try(record.getSNS().getMessage())

  override def handleRequest(event: SNSEvent, context: Context): Boolean = {
    val logger: LambdaLogger = context.getLogger
    logger.log("Lambda START\n")

    val recordSeq: Seq[SNSRecord] = event.getRecords().asScala.toSeq
    recordSeq.forall(record => {
      getSnsMessage(record) match {
        case Success(message) =>
          logger.log(message)
          true
        case Failure(e) =>
          logger.log(e.getMessage)
          false
      }
    })
  }
}

1.2 FAT JARファイル作成

sbt-assemblyでFAT JARを作成します。
コマンドを実行すると、target配下にfunction.jarというファイルが生成されます。

sbt assembly

1.3. Dockerイメージを定義

Dockerfileを作成し、Lambda関数用のDockerイメージを定義します。

FROM public.ecr.aws/lambda/java:11
COPY /target/function.jar ${LAMBDA_TASK_ROOT}/lib/
CMD ["com.example.LambdaHandler::handleRequest"]
  • FROM public.ecr.aws/lambda/java:11: Lambda関数用のベースイメージを指定します

  • COPY /target/function.jar ${LAMBDA_TASK_ROOT}/lib/: 先ほど作成したFAT JARファイルを、Lambda実行環境のライブラリディレクトリにコピーします

  • CMD ["com.example.LambdaHandler::handleRequest"]: Lambda関数のエントリーポイントを指定します

1.4 Dockerイメージ作成

以下のコマンドを実行して、Dockerイメージを作成します。

docker build --platform linux/x86_64 --no-cache -t sns-lambda:v1 .

1.5 ECRにリポジトリ作成

以下のコマンドを実行して、Amazon Elastic Container Registry(以降、ECRと表記します)にリポジトリを作成します。
--repository-name は任意のリポジトリ名に、--region はご自身の環境に合わせて変更してください。

aws ecr create-repository --repository-name sns-lambda-repository --region ap-northeast-1

成功すると、repositoryUri が出力されるので、メモしておきます。

1.6 Dockerイメージにタグ付け

作成したDockerイメージに、ECRリポジトリのURIを使ってタグ付けします。
[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sns-lambda-repository:v1 は、先ほどメモしたrepositoryUriに置き換えてください。

docker tag sns-lambda:v1 [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sns-lambda-repository:v1

1.7 ECR にログイン

ECRへDockerイメージをプッシュするために、AWSにログインします。

aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com

1.8 イメージを ECR へプッシュ

DockerイメージをECRへプッシュします。
[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sns-lambda-repository:v1は、先ほどメモしたrepositoryUriに置き換えてください。

docker push [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sns-lambda-repository:v1

2. Lambda関数作成

2.1 IAMロール作成

Lambda関数が実行時に必要な権限を持つIAMロールを作成します。

aws iam create-role \
  --role-name lambda-sns-role \
  --assume-role-policy-document \
'{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}'

出力されるArnをメモしておきます。

2.2 IAMロールにポリシーを付与

作成したロールに、Lambdaの基本的な実行権限を付与します。

aws iam attach-role-policy \
  --role-name lambda-sns-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

2.3 Lambda関数作成

以下のコマンドを実行して、Lambda関数を作成します。
ImageUriには、repositoryUriを、--roleには、IAMロール作成時にメモしたArnを指定してください。

aws lambda create-function \
  --function-name Function-With-SNS \
  --package-type Image \
  --code ImageUri=[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sns-lambda-repository:v1 \
  --role arn:aws:iam::[AWSアカウントID]:role/lambda-sns-role \
  --timeout 60

出力されるFunctionArnをメモしておきます。

3. SNSの設定

3.1 SNSトピック作成

SNSトピックを作成します。SNSトピックは、メッセージの送信先を識別するための論理アクセスポイントです。

引用: https://docs.aws.amazon.com/ja_jp/sns/latest/dg/sns-create-topic.html

aws sns create-topic --name sns-topic-for-lambda

出力されるTopicArnをメモしておきます。

3.2 SNSサブスクリプション作成(Lambda関数への紐付け)

作成したSNSトピックに、Lambda関数をサブスクライブ(紐付け)します。
これにより、SNSトピックにメッセージを送信すると、紐付けられたLambda関数が実行されます。
--topic-arnTopicArn--notification-endpointFunctionArnを指定します。

aws sns subscribe --protocol lambda \
  --region ap-northeast-1 \
  --topic-arn arn:aws:sns:ap-northeast-1:[AWSアカウントID]:sns-topic-for-lambda \
  --notification-endpoint arn:aws:lambda:ap-northeast-1:[AWSアカウントID]:function:Function-With-SNS

3.3 Lambda関数にリソースベースのポリシーを付与

リソースベースのポリシーをリソースにアタッチします。
--source-arn には、作成したTopicArnを指定します。

リソースベースのポリシーとは、誰がリソースにアクセスでき、リソースでどのようなアクションを実行できるかの権限を定義します。
このケースでは、SNSがLambda関数を呼び出すための権限を付与します。

引用: https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/access_policies_identity-vs-resource.html

aws lambda add-permission \
  --function-name Function-With-SNS \
  --statement-id AllowSNSInvoke \
  --action lambda:InvokeFunction \
  --principal sns.amazonaws.com \
  --source-arn arn:aws:sns:ap-northeast-1:[AWSアカウントID]:sns-topic-for-lambda

4. SNSにメッセージを送信する

作成したSNSトピックにメッセージを送信します。

aws sns publish --message 'Hello SNS' --subject Test \
  --topic-arn arn:aws:sns:ap-northeast-1:[AWSアカウントID]:sns-topic-for-lambda

5. ログの確認

Amazon CloudWatchのメニューから「ロググループ」を選択し、作成したLambda関数のロググループ(例:/aws/lambda/Function-With-SNS)をクリックします。

SNSから送信されたメッセージがLambdaで受け取れていることが確認できます。
スクリーンショット

まとめ

今回は、AWS CLIからSNSを呼び出し、SNSからLambdaへメッセージを送信する仕組みを実装しました。
今後も色々なサービス間の連携に挑戦していきたいと思います。
最後までお付き合いいただき、ありがとうございました!

nextbeat Tech Blog

Discussion