これは、なにをしたくて書いたもの?
前に、Serverless FrameworkをTypeScript(とLocalStack)で使ってみました。
Serverless FrameworkをLocalStack+TypeScriptで使ってみる(Amazon API Gateway+AWS Lambda) - CLOVER🍀
この時、テンプレートにaws-nodejs-typescript
を使用したのですが、Serverless Frameworkの設定ファイルがTypeScriptのファイルで
できるのが微妙に思い、ちょっと他の方法を探してみることにしました。
TypeScriptで書かれていても読めないことはないのですが、リファレンスはYAMLで書かれていますからね。
Serverless Framework - AWS Lambda Guide - Serverless.yml Reference
合わせていた方がいいのかな、と。
結論を書くと、JavaScript向けに作成したサービスにServerless Esbuildをプラグインとして追加する方法で良いかなと思います。
Serverless Plugin TypeScript
Serverless FrameworkとTypeScriptで検索すると、割と最初の方に見つかるのがServerss Plugin TypeScriptな気がします。
これは、Prisma Labsが作成して現在はServerlss Incがメンテナンスしているようです。
Originally developed by Prisma Labs, now maintained in scope of Serverless, Inc
なのですが、実際に使ってみるとエラーになります。
Error: Cannot access package artifact at ".esbuild/.serverless/hello.zip" (for "hello"): ENOENT: no such file or directory, access '/path/to/.build/.esbuild/.serverless/hello.zip'
こちらでも動作しないという話題が挙がっていて、今回はスルーすることにしました。
Serverless compose not working with typescript · Discussion #11218 · serverless/serverless · GitHub
Serverless Esbuild
今回使うのは、Serverless Esbuildです。
文字通り、esbuildでトランスパイルしてくれるプラグインです。前のエントリーでaws-nodejs-typescript
テンプレートでサービスを
作成した時にも、実は含まれていたものになります。
Serverless FrameworkをLocalStack+TypeScriptで使ってみる(Amazon API Gateway+AWS Lambda) - CLOVER🍀
今回は、こちらを使ってAmazon API Gateway+AWS Lambda関数を、LocalStackにデプロイするシナリオで試してみます。
環境
今回の環境は、こちら。
$ python3 -V Python 3.8.10 $ localstack --version 1.2.0
LocalStackを起動。
$ LAMBDA_EXECUTOR=docker-reuse localstack start
Node.jsのバージョン。
$ node --version v16.18.1 $ npm --version 8.19.2
$ aws --version aws-cli/2.9.1 Python/3.9.11 Linux/5.4.0-132-generic exe/x86_64.ubuntu.20 prompt/off $ awslocal --version aws-cli/2.9.1 Python/3.9.11 Linux/5.4.0-132-generic exe/x86_64.ubuntu.20 prompt/off
Serverless Frameworkのサービスを作成する
まずは、Serverless Frameworkのサービスを作成しましょう。
Serverless Framework Commands - AWS Lambda - Create
Serverless Frameworkをインストール。
$ npm i -D serverless $ npx serverless --version Framework Core: 3.25.0 (local) Plugin: 6.2.2 SDK: 4.3.2
aws-nodejs
テンプレートを使用して、サービスを作成します。
$ npx serverless create --template aws-nodejs
作成されたファイル。
$ ll 合計 340 drwxrwxr-x 3 xxxxx xxxxx 4096 11月 26 20:14 ./ drwxrwxr-x 24 xxxxx xxxxx 4096 11月 26 20:11 ../ -rw-r--r-- 1 xxxxx xxxxx 86 10月 12 11:50 .gitignore -rw-r--r-- 1 xxxxx xxxxx 444 10月 12 11:50 handler.js drwxrwxr-x 346 xxxxx xxxxx 12288 11月 26 20:13 node_modules/ -rw-rw-r-- 1 xxxxx xxxxx 309469 11月 26 20:14 package-lock.json -rw-rw-r-- 1 xxxxx xxxxx 105 11月 26 20:14 package.json -rw-r--r-- 1 xxxxx xxxxx 3282 11月 26 20:14 serverless.yml
主要なファイルの中身を見てみましょう。
serverless.yml
# Welcome to Serverless! # # This file is the main config file for your service. # It's very minimal at this point and uses default values. # You can always add more config options for more control. # We've included some commented out config examples here. # Just uncomment any of them to get that config option. # # For full config options, check the docs: # docs.serverless.com # # Happy Coding! service: serverless-plugin-esbuild-example # app and org for use with dashboard.serverless.com #app: your-app-name #org: your-org-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details frameworkVersion: '3' provider: name: aws runtime: nodejs12.x # you can overwrite defaults here # stage: dev # region: us-east-1 # you can add statements to the Lambda function's IAM Role here # iam: # role: # statements: # - Effect: "Allow" # Action: # - "s3:ListBucket" # Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] } # - Effect: "Allow" # Action: # - "s3:PutObject" # Resource: # Fn::Join: # - "" # - - "arn:aws:s3:::" # - "Ref" : "ServerlessDeploymentBucket" # - "/*" # you can define service wide environment variables here # environment: # variable1: value1 # you can add packaging information here #package: # patterns: # - '!exclude-me.js' # - '!exclude-me-dir/**' # - include-me.js # - include-me-dir/** functions: hello: handler: handler.hello # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details # events: # - httpApi: # path: /users/create # method: get # - websocket: $connect # - s3: ${env:BUCKET} # - schedule: rate(10 minutes) # - sns: greeter-topic # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 # - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx # - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx # - iot: # sql: "SELECT * FROM 'some_topic'" # - cloudwatchEvent: # event: # source: # - "aws.ec2" # detail-type: # - "EC2 Instance State-change Notification" # detail: # state: # - pending # - cloudwatchLog: '/aws/lambda/hello' # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp # - alb: # listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ # priority: 1 # conditions: # host: example.com # path: /hello # Define function environment variables here # environment: # variable2: value2 # you can add CloudFormation resource templates here #resources: # Resources: # NewResource: # Type: AWS::S3::Bucket # Properties: # BucketName: my-new-bucket # Outputs: # NewOutput: # Description: "Description for the output" # Value: "Some output value"
handler.js
'use strict'; module.exports.hello = async (event) => { return { statusCode: 200, body: JSON.stringify( { message: 'Go Serverless v1.0! Your function executed successfully!', input: event, }, null, 2 ), }; // Use this code if you don't use the http event with the LAMBDA-PROXY integration // return { message: 'Go Serverless v1.0! Your function executed successfully!', event }; };
ここから、TypeScriptに変更しつつ、Serverless Esbuildを導入していきます。
TypeScriptとServerless Esbuildの導入
TypeScriptや、Node.jsとAWS Lambdaに関する型定義を追加。
$ npm i -D typescript $ npm i -D prettier $ npm i -D @types/node@v16 @types/aws-lambda
Serverless Esbuildとesbuild、LocalStack Serverless Pluginを追加。
$ npm i -D serverless-esbuild esbuild $ npm i -D serverless-localstack
GitHub - localstack/serverless-localstack: ⚡ Serverless plugin for running against LocalStack
この時の依存関係は、このようになりました。
"devDependencies": { "@types/aws-lambda": "^8.10.109", "@types/node": "^16.18.3", "esbuild": "^0.15.15", "prettier": "^2.8.0", "serverless": "^3.25.0", "serverless-esbuild": "^1.33.2", "serverless-localstack": "^1.0.1", "typescript": "^4.9.3" },
package.json
のscripts
は、このようにしておきました。
"scripts": { "typecheck": "tsc --project .", "typecheck:watch": "tsc --project . --watch", "format": "prettier --write ./**/*.{js,ts}" }
TypeScriptの設定。
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/**/*" ] }
Prettierの設定。
.prettierrc.json
{ "singleQuote": true, "printWidth": 120 }
ソースコードは、TypeScriptで書き直しました。
handler.ts
import { APIGatewayEvent, APIGatewayProxyResult, ProxyResult } from 'aws-lambda'; export const hello = async (event: APIGatewayEvent): Promise<ProxyResult> => { return { statusCode: 200, body: JSON.stringify( { message: 'Go Serverless v1.0! Your function executed successfully!', input: event, }, null, 2 ), }; };
もともと生成されていたJavaScriptのソースコードは削除。
$ rm handler.js
serverless.yml
は、以下のように修正。
serverless.yml
service: serverless-plugin-esbuild-example frameworkVersion: '3' provider: name: aws runtime: nodejs16.x stage: dev region: us-east-1 package: individually: true functions: hello: handler: handler.hello events: - http: path: /hello method: get custom: esbuild: bundle: true minify: false target: node16 platform: node plugins: - serverless-esbuild - serverless-localstack
少し、説明を加えておきます。
今回利用するプラグイン。
plugins: - serverless-esbuild - serverless-localstack
AWS Lambda関数の定義。http
をイベントとして受け取るようにしたので、Amazon API Gatewayと連携します。
functions: hello: handler: handler.hello events: - http: path: /hello method: get
以下は、AWS Lambda関数を個別にパッケージングしてデプロイする設定です。今回はAWS Lambda関数はひとつだけなんですが。
package: individually: true
Packaging / Package Configuration / Packaging functions separately
あとは、esbuildの設定です。
custom: esbuild: bundle: true minify: false target: node16 platform: node
こちらは、Serverless Esbuildのドキュメントにある程度記載があります。
Serverless Esbuild / Configuration / Options
ただ、最終的にはesbuildのドキュメントを見ることになるのではないかな、と思います。
動作確認
準備ができたので、LocalStackにデプロイしましょう。
$ npx serverless deploy
デプロイが終わりました。
Using serverless-localstack Deploying serverless-plugin-esbuild-example to stage dev (us-east-1) Skipping template validation: Unsupported in Localstack ✔ Service deployed to stack serverless-plugin-esbuild-example-dev (13s) endpoint: http://localhost:4566/restapis/6t31uk9dr3/dev/_user_request_ functions: hello: serverless-plugin-esbuild-example-dev-hello (744 B) Need a better logging experience than CloudWatch? Try our Dev Mode in console: run "serverless --console"
エンドポイントの情報は、出力されていますね。
endpoint: http://localhost:4566/restapis/6t31uk9dr3/dev/_user_request_
Amazon API GatewayのREST APIのIDは、コマンドで取得した方が汎用的かなという気がするので、こちらで取得。
$ 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
{ "message": "Go Serverless v1.0! Your function executed successfully!", "input": { "path": "/hello", "headers": { "Host": "localhost:4566", "User-Agent": "curl/7.68.0", "accept": "*/*", "Content-Type": "application/json", "x-localstack-tgt-api": "apigateway", "X-Forwarded-For": "172.17.0.1, localhost:4566", "x-localstack-edge": "http://localhost:4566" }, "multiValueHeaders": { "Host": [ "localhost:4566" ], "User-Agent": [ "curl/7.68.0" ], "accept": [ "*/*" ], "Content-Type": [ "application/json" ], "x-localstack-tgt-api": [ "apigateway" ], "X-Forwarded-For": [ "172.17.0.1, localhost:4566" ], "x-localstack-edge": [ "http://localhost:4566" ] }, "body": "", "isBase64Encoded": false, "httpMethod": "GET", "queryStringParameters": null, "multiValueQueryStringParameters": null, "pathParameters": {}, "resource": "/hello", "requestContext": { "accountId": "000000000000", "apiId": "6t31uk9dr3", "resourcePath": "/hello", "domainPrefix": "localhost", "domainName": "localhost", "resourceId": "wyvlcgsfpz", "requestId": "88c77cbd-55ac-45bb-a531-c667f29cab02", "identity": { "accountId": "000000000000", "sourceIp": "172.17.0.1", "userAgent": "curl/7.68.0" }, "httpMethod": "GET", "protocol": "HTTP/1.1", "requestTime": "26/Nov/2022:12:50:59 +0000", "requestTimeEpoch": 1669467059223, "authorizer": {}, "path": "/dev/hello", "stage": "dev" }, "stageVariables": {} } }
OKですね。
まとめ
Serverless Esbuildを使って、TypeScriptのServerless Frameworkサービスを作ってみました。
そもそもの目的はserverless.ts
ではなく、serverless.yml
で設定を行いたい、だったのですが。aws-nodejs-typescript
テンプレートを
使わずにServerless FrameworkでTypeScriptを扱うにはどうすればよいのか、なかなか調べるのに苦労しました。
落ち着き先を振り返るとふつうの結果な気がしますが、自分の情報整理という点では意味があったかなと思います。