CLOVER🍀

That was when it all began.

Serverless Framework+LocalStackで、Amazon SNSのトピックをサブスクライブするAWS Lambda関数を作る

これは、なにをしたくて書いたもの?

今まで試してきたServerless Frameworkを使った例では、ずっとAmazon API Gatewayを使ったものだったのですが、別のものでも試して
みようかなと思いまして。

今回は、Amazon SNSを使ってみたいと思います。

Amazon SNS

Amazon SNSAmazon Simple Notification Service)は、メッセージの配信者(publisher)からメッセージの受信者(subscriber)への
メッセージ配信を行うAWSのサービスです。

Amazon SNS とは - Amazon Simple Notification Service

A2A(Application to Application: Amazon Kinesis Data Firehose、AWS Lambda関数、Amazon SQSキューなど)のメッセージ配信、
Application to Person(モバイルアプリケーション、携帯電話番号、メールなど)の通知、標準トピックとFIFOトピックなどの特徴が
あります。

特徴と機能 - Amazon Simple Notification Service

今回は単純にトピックを作成して

Amazon SNS トピックを作成する - Amazon Simple Notification Service

AWS Lambda関数でサブスクライブするようにします。

Amazon SNS トピックへサブスクライブする - Amazon Simple Notification Service

この部分は、Serverless Frameworkに担ってもらうわけですが。

というわけで、Amazon SNSトピックをサブスクライブする簡単なAWS Lambda関数を、Serverless Frameworkを使って実現してみたいと
思います。環境はLocalStackを使い、AWS Lambda関数はNode.jsとTypeScriptを使って作成します。

環境

今回の環境は、こちら。

$ python3 -V
Python 3.8.10


$ localstack --version
1.3.0

LocalStackを起動。

$ LAMBDA_EXECUTOR=docker-reuse localstack start

Node.js。

$ node --version
v16.18.1


$ npm --version
8.19.2

AWS CLIも使います。

$ awslocal --version
aws-cli/2.9.4 Python/3.9.11 Linux/5.4.0-135-generic exe/x86_64.ubuntu.20 prompt/off

Serverless Frameworkのサービスを作成する

まずは、Serverless Frameworkのサービスを作成します。

$ npm init -y

Serverless Frameworkのインストール。

$ npm i -D serverless


$ npx serverless --version
Framework Core: 3.25.1 (local)
Plugin: 6.2.2
SDK: 4.3.2

aws-nodejsテンプレートを使って、サービスを作成。

$ npx serverless create --template aws-nodejs

Serverless Framework Commands - AWS Lambda - Create

あとはTypeScriptやServerless Frameworkのプラグインなどをインストール。

$ npm i -D typescript
$ npm i -D @types/node@v16
$ npm i -D prettier


$ npm i -D @types/aws-lambda
$ npm i -D serverless-esbuild esbuild
$ npm i -D serverless-localstack

package.jsonscripts

  "scripts": {
    "typecheck": "tsc --project .",
    "typecheck:watch": "tsc --project . --watch",
    "format": "prettier --write ./**/*.{js,ts}"
  },

依存関係はそれぞれこんな感じに。

  "devDependencies": {
    "@types/aws-lambda": "^8.10.109",
    "@types/node": "^16.18.4",
    "esbuild": "^0.15.17",
    "prettier": "^2.8.0",
    "serverless": "^3.25.1",
    "serverless-esbuild": "^1.33.2",
    "serverless-localstack": "^1.0.1",
    "typescript": "^4.9.3"
  }

設定ファイル。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "moduleResolution": "node",
    "lib": ["esnext"],
    "noEmit": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": ["./**/*.ts"],
  "exclude": [
    "node_modules/**/*",
    ".serverless/**/*"
  ]
}

.prettierrc.json

{
  "singleQuote": true,
  "printWidth": 120
}

デフォルトで作成されるAWS Lambda関数のソースコードは、削除しておきます。

$ rm handler.js

Amazon SNSトピックをサブスクライブするAWS Lambda関数を作成する

では、Amazon SNSトピックをサブスクライブするAWS Lambda関数を作成しましょう。

内容はログ出力程度で、こんな感じに作成しました。

sns/handler.ts

import { SNSEvent } from 'aws-lambda';

export const receiver = async (event: SNSEvent): Promise<void> => {
  for (const record of event.Records) {
    const topicArn = record.Sns.TopicArn;
    const messageId = record.Sns.MessageId;
    const message = record.Sns.Message;

    console.log(`topicArn = ${topicArn}, messageId = ${messageId}, message = ${message}`);
  }
};

次に、Serverless Frameworkの設定を行います。

serverless.yml

service: serverless-sns

frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs16.x
  stage: dev
  region: us-east-1

package:
  individually: true

functions:
  snsEventReceiver:
    handler: sns/handler.receiver
    events:
      - sns:
          arn: !Ref MyTopic
          topicName: my-topic

resources:
  Resources:
    MyTopic:
      Type: AWS::SNS::Topic
      Properties:
        TopicName: my-topic


custom:
  esbuild:
    bundle: true
    target: node16
    platform: node

plugins:
  - serverless-esbuild
  - serverless-localstack

今回は、Serverless FrameworkにAmazon SNSトピックを作成してもらうことにします。

とすると、実は以下の内容だけで実現できたりするんですよね。

functions:
  snsEventReceiver:
    handler: sns/handler.receiver
    events:
       - sns: my-topic

これで、デプロイ時にServerless Frameworkがsnsで指定したトピック名でAmazon SNSトピックを作成してくれます。

今回は、明示的にAmazon SNSのリソース定義まで行ってみました。

functions:
  snsEventReceiver:
    handler: sns/handler.receiver
    events:
      - sns:
          arn: !Ref MyTopic
          topicName: my-topic

resources:
  Resources:
    MyTopic:
      Type: AWS::SNS::Topic
      Properties:
        TopicName: my-topic

こちらを参考に。

Serverless Framework - AWS Lambda Events - SNS

トピック名については、GetAtt関数を使ってこんな感じで参照しようかなと思ったのですが、

functions:
  snsEventReceiver:
    handler: sns/handler.receiver
    events:
      - sns:
          arn: !Ref MyTopic
          topicName: !GetAtt MyTopic.TopicName
        
resources:
  Resources:
    MyTopic:
      Type: AWS::SNS::Topic
      Properties:
        TopicName: my-topic

これはうまくいかなかったので、諦めました…。

Error:
TypeError: name.replace is not a function
    at Object.normalizeNameToAlphaNumericOnly (/path/to/node_modules/serverless/lib/plugins/aws/lib/naming.js:32:36)
    at Object.normalizeTopicName (/path/to/node_modules/serverless/lib/plugins/aws/lib/naming.js:359:17)
    at Object.getLambdaSnsSubscriptionLogicalId (/path/to/node_modules/serverless/lib/plugins/aws/lib/naming.js:505:82)
    at /path/to/node_modules/serverless/lib/plugins/aws/package/compile/events/sns.js:185:64
    at Array.forEach (<anonymous>)
    at /path/to/node_modules/serverless/lib/plugins/aws/package/compile/events/sns.js:63:28
    at Array.forEach (<anonymous>)
    at AwsCompileSNSEvents.compileSNSEvents (/path/to/node_modules/serverless/lib/plugins/aws/package/compile/events/sns.js:59:47)
    at PluginManager.runHooks (/path/to/node_modules/serverless/lib/classes/plugin-manager.js:530:15)
    at async PluginManager.invoke (/path/to/node_modules/serverless/lib/classes/plugin-manager.js:564:9)
    at async PluginManager.spawn (/path/to/node_modules/serverless/lib/classes/plugin-manager.js:585:5)
    at async before:deploy:deploy (/path/to/node_modules/serverless/lib/plugins/deploy.js:40:11)
    at async PluginManager.runHooks (/path/to/node_modules/serverless/lib/classes/plugin-manager.js:530:9)
    at async PluginManager.invoke (/path/to/node_modules/serverless/lib/classes/plugin-manager.js:563:9)
    at async PluginManager.run (/path/to/node_modules/serverless/lib/classes/plugin-manager.js:604:7)
    at async Serverless.run (/path/to/node_modules/serverless/lib/serverless.js:170:5)
    at async /path/to/node_modules/serverless/scripts/serverless.js:787:9

1 deprecation found: run 'serverless doctor' for more details

では、デプロイしてみます。

$ npx serverless deploy

うまくいったようです。

Using serverless-localstack

Deploying serverless-sns to stage dev (us-east-1)
Skipping template validation: Unsupported in Localstack

✔ Service deployed to stack serverless-sns-dev (13s)

functions:
  snsEventReceiver: serverless-sns-dev-snsEventReceiver (744 B)

Need a better logging experience than CloudWatch? Try our Dev Mode in console: run "serverless --console"

Amazon SNSトピックが作成されていることを確認。

$ awslocal sns list-topics
{
    "Topics": [
        {
            "TopicArn": "arn:aws:sns:us-east-1:000000000000:my-topic"
        }
    ]
}

メッセージを送信してみます。

$ awslocal sns publish --topic-arn arn:aws:sns:us-east-1:000000000000:my-topic --message 'Hello SNS!!'

こちらのページにはAWS CLIを使ったメッセージ送信方法は書いていませんでしたが、publisでできるみたいです…。

Amazon SNS メッセージの発行 - Amazon Simple Notification Service

Dockerコンテナのログを見て確認。

$ docker container logs -f localstack_main_lambda_arn_aws_lambda_us-east-1_000000000000_function_serverless-sns-dev-snsEventReceiver

OKですね。

START RequestId: d0975886-d370-137c-30dd-fd2889ee1272 Version: $LATEST
2022-12-04T14:51:46.534Z        d0975886-d370-137c-30dd-fd2889ee1272    INFO    topicArn = arn:aws:sns:us-east-1:000000000000:my-topic, messageId = ccaed308-8afd-42cd-bccf-c72388d68528, message = Hello SNS!!
END RequestId: d0975886-d370-137c-30dd-fd2889ee1272
REPORT RequestId: d0975886-d370-137c-30dd-fd2889ee1272  Duration: 3.92 ms       Billed Duration: 4 ms   Memory Size: 1536 MB    Max Memory Used: 44 MB

まとめ

Serverless Framework+LocalStackで、Amazon SNSトピックをサブスクライブするAWS Lambda関数を作ってみました。

AWS CloudFormationの関数を使おうとしてちょっとハマりましたが、それ以外は特に困らなかったですね。