CLOVER🍀

That was when it all began.

Serverless Esbuildを使って、TypeScriptのServerless Frameworkサービスを作る

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

前に、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な気がします。

Serverless Framework: Plugins

これは、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です。

Serverless Framework: Plugins

文字通り、esbuildでトランスパイルしてくれるプラグインです。前のエントリーでaws-nodejs-typescriptテンプレートでサービスを
作成した時にも、実は含まれていたものになります。

Serverless FrameworkをLocalStack+TypeScriptで使ってみる(Amazon API Gateway+AWS Lambda) - CLOVER🍀

今回は、こちらを使ってAmazon API GatewayAWS 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 CLIも使います。

$ 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

Serverless Esbuild / Install

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.jsonscriptsは、このようにしておきました。

  "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のドキュメントを見ることになるのではないかな、と思います。

esbuild - API

動作確認

準備ができたので、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 GatewayREST 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を扱うにはどうすればよいのか、なかなか調べるのに苦労しました。

落ち着き先を振り返るとふつうの結果な気がしますが、自分の情報整理という点では意味があったかなと思います。