CLOVER🍀

That was when it all began.

Serverless FrameworkでAWS Lambda関数をローカル環境でエミュレーションで動かす

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

これまでServerless Frameworkを使って作成したサービスをLocalStackにデプロイして動かしていましたが、デプロイせずとも動作確認する
パターンも試しておきたいなと思いまして。

invoke localとgenerate-event

使うのは、こちらの2つのコマンドかなと思います。invoke localとgenerate-eventです。

Serverless Framework Commands - AWS Lambda - Invoke Local

Serverless Framework Commands - AWS Lambda - Generate Event

invoke localは、AWS Lambdaをエミュレーションすることでローカルで実行するコマンドです。

Serverless Framework Commands - AWS Lambda - Invoke Local

対みたいなものとして、AWS環境にデプロイされたAWS Lambda関数を呼び出すコマンドとしてinvokeがあります。

Serverless Framework Commands - AWS Lambda - Invoke

invoke localはあくまでエミュレーションであり、完璧ではありません。それでも多くのユーザーの役には立つだろうとしています。
また、contextはモックデータになります。

Please keep in mind, it's not a 100% perfect emulation, there may be some differences, but it works for the vast majority of users. We mock the context with simple mock data.

以下のようにして、ローカルで関数を呼び出します。

$ npx serverless invoke local --function [function-name]

入力となるデータは、以下のパターンで渡せるようです。

## --dataオプション
$ npx serverless invoke local --function [function-name] --data '{"a":"bar"}'


## --raw(JSONデータ以外を渡す場合) + --dataオプション
$ npx serverless invoke local --function [function-name] --raw --data "hello world"


## 標準入力から渡す
$ [data generate command] | npx serverless invoke local --function [function-name]


## --pathオプションでファイルを指定する
$ npx serverless invoke local --function [function-name] --path [JSON file path]

制限事項としては、Node.js、Python、Java、Ruby以外のランタイムを使用するか、--dockerオプションを使用するとDockerが必要に
なる点ですね。

AWS - Invoke Local / Examples / Limitations

また、AWS IAMのエミュレーションも完全ではありません。

AWS - Invoke Local / Resource permissions

ここまでで、データがあればAWS Lambda関数をローカルでエミュレーション的に動かせそうなことはわかりました。

ちなみに、Amazon API Gateway+AWS Lambdaであれば、似た用途でServerless Offlineというプラグインもあるようです。

GitHub - dherault/serverless-offline: Emulate AWS λ and API Gateway locally when developing your Serverless project

こちらは、またの機会にしておくとします。

あとはAWS Lambdaを呼び出すのに必要なデータの作成ですが、これにはgenerate-eventを使うのが良さそうです。

Serverless Framework Commands - AWS Lambda - Generate Event

generate-eventコマンドは、様々なイベントに対するサンプルのペイロードを作成します。

以下が使えるようです。

これをinvoke localに標準入力として与えるなり、ファイルに保存して使うなりするとよさそうです。

早速試してみましょう。今回は、Amazon API GatewayとAmazon SNSの2つのAWS Lambda関数を用意して試してみます。

環境

今回の環境は、こちら。

$ node --version
v16.18.1


$ npm --version
8.19.2

最初に、AWS Lambda関数が作成できているかどうかを確認するのにLocalStackを使うことにします。

$ python3 -V
Python 3.8.10


$ localstack --version
1.3.0

起動。

$ LAMBDA_EXECUTOR=docker-reuse localstack start

動作確認の一部で使用する、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プロジェクトの作成。

$ npm init -y

Serverless Framworkのインストール。

$ npm i -D serverless


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

aws-nodejsテンプレートを使用して、Serverless Frameworkサービスの作成。

$ npx serverless create --template aws-nodejs

TypeScriptや、AWS Lambdaの型定義、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

依存関係は、このようになりました。

  "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"
  }

scriptsの定義。

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

各種設定ファイル。

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
}

もともと作成されていたソースコードは削除。

$ rm handler.js

Amazon API Gateway向けのAWS Lambda関数を作成。

api-gateway/hello.ts

import { APIGatewayEvent, ProxyResult } from 'aws-lambda';

export const handler = async (event: APIGatewayEvent): Promise<ProxyResult> => {
  const queryString = event.queryStringParameters;

  let queryParam;
  if (queryString != null) {
    queryParam = queryString['queryParam'];
  } else {
    queryParam = '';
  }

  const body = event.body;

  console.log(`queryParam = ${queryParam}, body = ${body}`);

  return {
    statusCode: 200,
    body: JSON.stringify({
      queryParam: queryParam,
      body: body,
    }),
  };
};

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

sns/hello.ts

import { SNSEvent } from 'aws-lambda';

export const handler = 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-invoke-local

frameworkVersion: '3'

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

package:
  individually: true

functions:
  helloHttp:
    handler: api-gateway/hello.handler
    events:
      - http:
          path: /hello
          method: post
  helloSns:
    handler: sns/hello.handler
    events:
      - sns: my-topic

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

plugins:
  - serverless-esbuild
  - serverless-localstack

まずは、LocalStackにデプロイして動作確認しておきましょう。

$ npx serverless deploy

確認。

Amazon API Gateway+AWS Lambda。

$ REST_API_ID=$(awslocal apigateway get-rest-apis --query 'reverse(sort_by(items[], &createdDate))[0].id' --output text)


$ curl -H 'Content-Type: application/json' http://localhost:4566/restapis/$REST_API_ID/dev/_user_request_/hello?queryParam=foo -d '{"message": "Hello World"}'
{"queryParam":"foo","body":"{\"message\": \"Hello World\"}"}

ログ。

START RequestId: b7adf82c-c85f-17f1-f480-98d3aa89311c Version: $LATEST
2022-12-06T15:04:24.545Z        b7adf82c-c85f-17f1-f480-98d3aa89311c    INFO    queryParam = foo, body = {"message": "Hello World"}
END RequestId: b7adf82c-c85f-17f1-f480-98d3aa89311c
REPORT RequestId: b7adf82c-c85f-17f1-f480-98d3aa89311c  Init Duration: 3483.41 ms       Duration: 22.58 ms      Billed Duration: 23 ms  Memory Size: 1536 MB    Max Memory Used: 43 MB

Amazon SNS+AWS Lambda。

$ TOPIC_ARN=$(awslocal sns list-topics --query Topics[0].TopicArn --output text)


$ awslocal sns publish --topic-arn $TOPIC_ARN --message 'Hello SNS!!'
{
    "MessageId": "738ee38d-eef0-4523-bde0-5e816b8ea709"
}

ログ。

START RequestId: 676c3213-c3fa-10f3-db33-4a6fc295d60c Version: $LATEST
2022-12-06T15:07:36.575Z        676c3213-c3fa-10f3-db33-4a6fc295d60c    INFO    topicArn = arn:aws:sns:us-east-1:000000000000:my-topic, messageId = 738ee38d-eef0-4523-bde0-5e816b8ea709, message = Hello SNS!!
END RequestId: 676c3213-c3fa-10f3-db33-4a6fc295d60c
REPORT RequestId: 676c3213-c3fa-10f3-db33-4a6fc295d60c  Init Duration: 131.38 ms        Duration: 7.31 ms       Billed Duration: 8 ms   Memory Size: 1536 MB    Max Memory Used: 28 MB

OKですね。

確認は終わったので、LocalStackは停止しておきます。

また、Serverless LocalStackもserverless.ymlから外しておきましょう。

plugins:
  - serverless-esbuild
  # - serverless-localstack

そのままでも問題ないのですが、標準出力に「Using serverless-localstack」という表示がついて回るので、今回は外しておきました。

generate-eventでデータを作成し、invoke localでAWS Lambda関数をローカルで動かしてみる

では、今回の本題であるServerless Frameworkを使ったローカルでのAWS Lambda関数の実行を試してみます。

とりあえずデータがないと始まらないので、generate-eventを動かしてみましょう。

-t(または--type)オプションを使って、生成したいイベントを指定します。

aws:apiGatewayで、Amazon API Gatewayです。

$ npx serverless generate-event -t aws:apiGateway
{"body":"{}","path":"/test/hello","headers":{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8","Accept-Encoding":"gzip, deflate, lzma, sdch, br","Accept-Language":"en-US,en;q=0.8","CloudFront-Forwarded-Proto":"https","CloudFront-Is-Desktop-Viewer":"true","CloudFront-Is-Mobile-Viewer":"false","CloudFront-Is-SmartTV-Viewer":"false","CloudFront-Is-Tablet-Viewer":"false","CloudFront-Viewer-Country":"US","Host":"wt6mne2s9k.execute-api.us-west-2.amazonaws.com","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48","Via":"1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)","X-Amz-Cf-Id":"nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==","X-Forwarded-For":"192.168.100.1, 192.168.1.1","X-Forwarded-Port":"443","X-Forwarded-Proto":"https"},"pathParameters":{"proxy":"hello"},"multiValueHeaders":{},"isBase64Encoded":false,"multiValueQueryStringParameters":{},"requestContext":{"accountId":"123456789012","resourceId":"us4z18","stage":"test","requestId":"41b45ea3-70b5-11e6-b7bd-69b5aaebc7d9","identity":{"accessKey":"","apiKeyId":"","cognitoIdentityPoolId":"","accountId":"","cognitoIdentityId":"","caller":"","apiKey":"","sourceIp":"192.168.100.1","cognitoAuthenticationType":"","cognitoAuthenticationProvider":"","userArn":"","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48","user":""},"path":"","requestTimeEpoch":0,"resourcePath":"/{proxy+}","httpMethod":"GET","apiId":"wt6mne2s9k"},"resource":"/{proxy+}","httpMethod":"GET","queryStringParameters":{"name":"me"},"stageVariables":{"stageVarName":"stageVarValue"}}

aws:snsで、Amazon SNSです。

$ npx serverless generate-event --type aws:sns
{"Records":[{"EventSource":"aws:sns","EventVersion":"1.0","EventSubscriptionArn":"arn:aws:sns:us-east-1:123456789:service-1474781718017-1:fdaa4474-f0ff-4777-b1c4-79b96f5a504f","Sns":{"Type":"Notification","MessageId":"52ed5e3d-5fgf-56bf-923d-0e5c3b503c2a","TopicArn":"arn:aws:sns:us-east-1:123456789:service-1474781718017-1","Subject":"","Message":"{}","Timestamp":"2016-09-25T05:37:51.150Z","SignatureVersion":"1","Signature":"V5QL/dhow62Thr9PXYsoHA7bOsDFkLdWZVd8D6LyptA6mrq0Mvldvj/XNtai3VaPp84G3bD2nQbiuwYbYpu9u9uHZ3PFMAxIcugV0dkOGWmYgKxSjPApItIoAgZyeH0HzcXHPEUXXO5dVT987jZ4eelD4hYLqBwgulSsECO9UDCdCS0frexiBHRGoLbWpX+2Nf2AJAL+olEEAAgxfiPEJ6J1ArzfvTFZXdd4XLAbrQe+4OeYD2dw39GBzGXQZemWDKf4d52kk+SwXY1ngaR4UfExQ10lDpKyfBVkSwroaq0pzbWFaxT2xrKIr4sk2s78BsPk0NBi55xA4k1E4tr9Pg==","SigningCertUrl":"https://sns.us-east-1.amazonaws.com/SimpleNotificationService-b95095beb82e8f6a0e6b3aafc7f4149a.pem","UnsubscribeUrl":"https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456789:service-1474781718017-1:fdaa4474-f0ff-4777-b1c4-79b96f5a504f","MessageAttributes":{}}}]}

サポートしているイベントは、--helpでも確認できます。

$ npx serverless generate-event --help
generate-event                  Generate event
--type / -t (required)          Specify event type. "aws:apiGateway", "aws:sns", "aws:sqs", "aws:dynamo", "aws:kinesis", "aws:cloudWatchLog", "aws:s3", "aws:alexaSmartHome", "aws:alexaSkill", "aws:cloudWatch", "aws:iot", "aws:cognitoUserPool","aws:websocket" are supported.
--body / -b                     Specify the body for the message, request, or stream event.
--help / -h                     Show this message
--version / -v                  Show version info
--verbose                       Show verbose logs
--debug                         Namespace of debug logs to expose (use "*" to display all)

とりあえず、呼び出してみましょう。Amazon API Gatewayから。標準入力として渡してみましょう。

$ npx serverless generate-event -t aws:apiGateway | npx serverless invoke local --function helloHttp
queryParam = undefined, body = {}
{
    "statusCode": 200,
    "body": "{\"body\":\"{}\"}"
}

ローカルで動かしているので、標準出力に書き出したログはそのままコンソールに表示されます。

なんかエスケープが多いような?と思ったのですが、これはAWS Lambda関数の戻り値がそのまま見えているからですね。

curlでLocalStack越しに呼び出した時は、こんな感じでstatusCodeもなかったですし。

$ curl -H 'Content-Type: application/json' http://localhost:4566/restapis/$REST_API_ID/dev/_user_request_/hello?queryParam=foo -d '{"message": "Hello World"}'
{"queryParam":"foo","body":"{\"message\": \"Hello World\"}"}

Amazon SNS。

$ npx serverless generate-event --type aws:sns | npx serverless invoke local --function helloSns
topicArn = arn:aws:sns:us-east-1:123456789:service-1474781718017-1, messageId = 52ed5e3d-5fgf-56bf-923d-0e5c3b503c2a, message = {}

こちらは、戻り値はないAWS Lambda関数でした。

どちらもボディを指定していないので、中身が空っぽですね。

追加のオプションとして、-b(または--body)オプションで、ボディを指定することもできます。

$ npx serverless generate-event -t aws:apiGateway -b '{"message": "Hello World"}' | npx serverless invoke local --function helloHttp
queryParam = undefined, body = {"message": "Hello World"}
{
    "statusCode": 200,
    "body": "{\"body\":\"{\\\"message\\\": \\\"Hello World\\\"}\"}"
}


$ npx serverless generate-event --type aws:sns --body 'Hello SNS!!' | npx serverless invoke local --function he
lloSns
topicArn = arn:aws:sns:us-east-1:123456789:service-1474781718017-1, messageId = 52ed5e3d-5fgf-56bf-923d-0e5c3b503c2a, message = Hello SNS!!

OKですね。

あとは、1度ファイルに保存して編集してもよいでしょう。

$ mkdir events
$ npx serverless generate-event -t aws:apiGateway -b '{"message": "Hello World"}' | jq > events/api-gateway.json
$ npx serverless generate-event -t aws:sns -b 'Hello SNS!!' | jq > events/sns.json

データは、jqで整形して出力しました。

ボディについては生成後に修正してもよいのですが、JSON内に文字列として埋め込むことになるので、可能なら-b(--body)オプションで
指定して生成した方がよいかもですね。エスケープもしてくれるので。

生成後、少し修正したデータ。

events/api-gateway.json

{
  "body": "{\"message\": \"Hello World\"}",
  "path": "/test/hello",
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate, lzma, sdch, br",
    "Accept-Language": "en-US,en;q=0.8",
    "CloudFront-Forwarded-Proto": "https",
    "CloudFront-Is-Desktop-Viewer": "true",
    "CloudFront-Is-Mobile-Viewer": "false",
    "CloudFront-Is-SmartTV-Viewer": "false",
    "CloudFront-Is-Tablet-Viewer": "false",
    "CloudFront-Viewer-Country": "US",
    "Host": "wt6mne2s9k.execute-api.us-west-2.amazonaws.com",
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48",
    "Via": "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)",
    "X-Amz-Cf-Id": "nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==",
    "X-Forwarded-For": "192.168.100.1, 192.168.1.1",
    "X-Forwarded-Port": "443",
    "X-Forwarded-Proto": "https"
  },
  "pathParameters": {
    "proxy": "hello"
  },
  "multiValueHeaders": {},
  "isBase64Encoded": false,
  "multiValueQueryStringParameters": {},
  "requestContext": {
    "accountId": "123456789012",
    "resourceId": "us4z18",
    "stage": "test",
    "requestId": "41b45ea3-70b5-11e6-b7bd-69b5aaebc7d9",
    "identity": {
      "accessKey": "",
      "apiKeyId": "",
      "cognitoIdentityPoolId": "",
      "accountId": "",
      "cognitoIdentityId": "",
      "caller": "",
      "apiKey": "",
      "sourceIp": "192.168.100.1",
      "cognitoAuthenticationType": "",
      "cognitoAuthenticationProvider": "",
      "userArn": "",
      "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48",
      "user": ""
    },
    "path": "",
    "requestTimeEpoch": 0,
    "resourcePath": "/{proxy+}",
    "httpMethod": "GET",
    "apiId": "wt6mne2s9k"
  },
  "resource": "/{proxy+}",
  "httpMethod": "GET",
  "queryStringParameters": {
    "queryParam": "foo"
  },
  "stageVariables": {
    "stageVarName": "stageVarValue"
  }
}

events/sns.json

{
  "Records": [
    {
      "EventSource": "aws:sns",
      "EventVersion": "1.0",
      "EventSubscriptionArn": "arn:aws:sns:us-east-1:123456789:service-1474781718017-1:fdaa4474-f0ff-4777-b1c4-79b96f5a504f",
      "Sns": {
        "Type": "Notification",
        "MessageId": "52ed5e3d-5fgf-56bf-923d-0e5c3b503c2a",
        "TopicArn": "arn:aws:sns:us-east-1:123456789:service-1474781718017-1",
        "Subject": "",
        "Message": "Hello SNS!!",
        "Timestamp": "2016-09-25T05:37:51.150Z",
        "SignatureVersion": "1",
        "Signature": "V5QL/dhow62Thr9PXYsoHA7bOsDFkLdWZVd8D6LyptA6mrq0Mvldvj/XNtai3VaPp84G3bD2nQbiuwYbYpu9u9uHZ3PFMAxIcugV0dkOGWmYgKxSjPApItIoAgZyeH0HzcXHPEUXXO5dVT987jZ4eelD4hYLqBwgulSsECO9UDCdCS0frexiBHRGoLbWpX+2Nf2AJAL+olEEAAgxfiPEJ6J1ArzfvTFZXdd4XLAbrQe+4OeYD2dw39GBzGXQZemWDKf4d52kk+SwXY1ngaR4UfExQ10lDpKyfBVkSwroaq0pzbWFaxT2xrKIr4sk2s78BsPk0NBi55xA4k1E4tr9Pg==",
        "SigningCertUrl": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-b95095beb82e8f6a0e6b3aafc7f4149a.pem",
        "UnsubscribeUrl": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456789:service-1474781718017-1:fdaa4474-f0ff-4777-b1c4-79b96f5a504f",
        "MessageAttributes": {}
      }
    }
  ]
}

こちらを使って、invoke localでAWS Lambda関数を呼び出してみます。--pathオプションを使います。

$ npx serverless invoke local --function helloHttp --path events/api-gateway.json
queryParam = foo, body = {"message": "Hello World"}
{
    "statusCode": 200,
    "body": "{\"queryParam\":\"foo\",\"body\":\"{\\\"message\\\": \\\"Hello World\\\"}\"}"
}


$ npx serverless invoke local --function helloSns --path events/sns.json
topicArn = arn:aws:sns:us-east-1:123456789:service-1474781718017-1, messageId = 52ed5e3d-5fgf-56bf-923d-0e5c3b503c2a, message = Hello SNS!!

OKですね。

まとめ

Serverless Frameworkで扱っているAWS Lambda関数を、invoke localで呼び出し、そのデータにはgenerate-eventを使うことを
試してみました。

実際に動かすのに必要なものはAWS Lambdaとトリガーとなるリソースだけ、というわけにはなかなかいかないと思いますが、
簡単な動作確認には良いのではないでしょうか?

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

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

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

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

Amazon SNS

Amazon SNS(Amazon 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.jsonのscripts、

  "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の関数を使おうとしてちょっとハマりましたが、それ以外は特に困らなかったですね。