これは、なにをしたくて書いたもの?
これまで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というプラグインもあるようです。
こちらは、またの機会にしておくとします。
あとはAWS Lambdaを呼び出すのに必要なデータの作成ですが、これにはgenerate-event
を使うのが良さそうです。
Serverless Framework Commands - AWS Lambda - Generate Event
generate-event
コマンドは、様々なイベントに対するサンプルのペイロードを作成します。
以下が使えるようです。
- aws:alexaSkill
- aws:alexaSmartHome
- aws:apiGateway
- aws:cloudWatch
- aws:cloudWatchLog
- aws:cognitoUserPool
- aws:dynamo
- aws:iot
- aws:kinesis
- aws:s3
- aws:sns
- aws:sqs
- aws:websocket
これを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
$ 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
$ 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"}}
$ 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\"}"}
$ 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とトリガーとなるリソースだけ、というわけにはなかなかいかないと思いますが、
簡単な動作確認には良いのではないでしょうか?