JavaScript
Node.js
AWS
TypeScript
serverless

ServerlessでTypeScriptの開発環境を作る

Lambdaを使ったプロジェクトの移行に伴い、せっかくだからServerlessとTypeScriptで開発できる環境を作ろうぜ!
と思ったら結構ハマったので環境構築手順を記録しておきます。

基本編

Serverlessのインストール

serverlessをインストールします。TypeScriptが使えるのは v1.21.0 からなので注意してください。

# serverlessのインストール
npm i -g serverless
# バージョン確認
sls -v 
# -> 1.32.0

プロジェクトの作成

# プロジェクトディレクトリの作成場所に移動
cd 任意のディレクトリ
# TypeScriptプロジェクトの作成
sls create -t aws-nodejs-typescript -p sls-test

sls create コマンドでプロジェクトの雛形を作成できます。
各パラメータの意味は次のとおりです。

  • -t - テンプレートのタイプ。TypeScriptの場合はaws-nodejs-typescriptを指定します。
  • -p - プロジェクトの名前。今回はsls-testとしました。

プロジェクトの作成に成功すると、プロジェクト名と同じフォルダが作成されます。

中身の確認

まずはserverless.ymlファイルを開いてみてください。

serverless.yml
service:
  name: sls-test

# Add the serverless-webpack plugin
plugins:
  - serverless-webpack

provider:
  name: aws
  runtime: nodejs8.10

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          method: get
          path: hello

functionキー以下に、このプロジェクトで管理するLambda関数の定義を並べていきます。
デフォルトでは、雛形としてhelloという関数が用意されていて、handler.helloがこの関数のエントリポイントとして設定されています。
このhandler.helloは、 「handler.tsファイル内のhello関数を呼び出すよ」 という意味で、その具体的な実装はhandler.tsファイル内に書かれています。

handler.ts
import { APIGatewayEvent, Callback, Context, Handler } from 'aws-lambda';

export const hello: Handler = (event: APIGatewayEvent, context: Context, cb: Callback) => {
  const response = {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Go Serverless Webpack (Typescript) v1.0! Your function executed successfully!',
      input: event,
    }),
  };

  cb(null, response);
}

関数を増やす場合はエントリポイントのコードを追加して、serverless.ymlにそのエントリコードへのパスを記述します。

デプロイ

雛形がインストールできたので、動作確認してみます。
AWSにプロジェクト全体をデプロイするにはsls deployコマンドを実行します。

sls deploy

すると、こんなエラーが発生しました。

実行結果
  Serverless Error ---------------------------------------

  Serverless plugin "serverless-webpack" not found. Make sure it's installed and listed in the "plugins" section of your serverless config file.

  Get Support --------------------------------------------
     Docs:          docs.serverless.com
     Bugs:          github.com/serverless/serverless/issues
     Issues:        forum.serverless.com

  Your Environment Information -----------------------------
     OS:                     win32
     Node Version:           9.2.0
     Serverless Version:     1.32.0

serverless-webpack プラグインがない」と言われています。
package.jsonを見ると、serverless-webpackの記載がありますが、プロジェクトフォルダ内にnode_modulesフォルダがありません。
デフォルトではインストールされていないようです。
(もしくは事前にグローバルインストールしておくのが前提?)

package.json
{
  "name": "sls-test",
  "version": "1.0.0",
  "description": "Serverless webpack example using Typescript",
  "main": "handler.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {},
  "devDependencies": {
    "@types/aws-lambda": "8.10.1",
    "@types/node": "^8.0.57",
    "serverless-webpack": "^5.1.1",
    "source-map-support": "^0.5.6",
    "ts-loader": "^4.2.0",
    "typescript": "^2.9.2",
    "webpack": "^4.5.0"
  },
  "author": "The serverless webpack authors (https://github.com/elastic-coders/serverless-webpack)",
  "license": "MIT"
}

ということで、まずプロジェクトフォルダ内でnpm installコマンドを実行し、改めてsls deployコマンドを実行します。

npm install
sls deploy
実行結果
Serverless: Bundling with Webpack...
Time: 4047ms
Built at: 2018-10-09 10:28:45
         Asset      Size  Chunks             Chunk Names
    handler.js  33.5 KiB       0  [emitted]  handler
handler.js.map   170 KiB       0  [emitted]  handler
Entrypoint handler = handler.js handler.js.map
 [1] ./node_modules/source-map-support/node_modules/source-map/lib/source-map-generator.js 14 KiB {0} [built]
 [2] ./node_modules/source-map-support/node_modules/source-map/lib/base64-vlq.js 4.6 KiB {0} [built]
 [3] ./node_modules/source-map-support/node_modules/source-map/lib/array-set.js 3.12 KiB {0} [built]
 [4] multi ./source-map-install.js ./handler.ts 40 bytes {0} [built]
 [5] ./source-map-install.js 41 bytes {0} [built]
 [6] ./node_modules/source-map-support/source-map-support.js 17.5 KiB {0} [built]
 [7] ./node_modules/source-map-support/node_modules/source-map/source-map.js 405 bytes {0} [built]
[10] ./node_modules/source-map-support/node_modules/source-map/lib/source-map-consumer.js 39.6 KiB {0} [built]
[12] ./node_modules/source-map-support/node_modules/source-map/lib/quick-sort.js 3.53 KiB {0} [built]
[13] ./node_modules/source-map-support/node_modules/source-map/lib/source-node.js 13.5 KiB {0} [built]
[14] external "path" 42 bytes {0} [built]
[15] external "fs" 42 bytes {0} [optional] [built]
[16] ./node_modules/buffer-from/index.js 1.56 KiB {0} [built]
[17] external "module" 42 bytes {0} [optional] [built]
[18] ./handler.ts 310 bytes {0} [built]
    + 4 hidden modules
Serverless: Packaging service...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (68.59 KB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..............................
Serverless: Stack update finished...
Service Information
service: sls-test
stage: dev
region: us-east-1
stack: sls-test-dev
api keys:
  None
endpoints:
  GET - https://*****.execute-api.us-east-1.amazonaws.com/dev/hello
functions:
  hello: sls-test-dev-hello

実行結果の最後のほうをみると、API GatewayのエンドポイントURLと、Lambda関数名のようなものが見えます。
AWSコンソールから確認してみます。

Lambda.PNG
キャプチャ1.png

sls-test-dev-helloというLambdaができていて、それがAPI Gatewayから呼び出されるようになっています。

※注意
serverlessが生成したLambdaやAPI Gatewayのエンドポイントを手動で削除しないでください。同期が取れなくなり、以降のデプロイに失敗します。
関数を削除するには、serverless.ymlから関数の定義を削除して、sls deployコマンドで全体デプロイをしなおします。

動作確認

API Gatewayのエンドポイントにアクセスしてもいいですが、コマンドからLambda関数を直接実行して動作確認することもできます。

sls deploy -f hello
実行結果
{
    "statusCode": 200,
    "body": "{\"message\":\"Go Serverless Webpack (Typescript) v1.0! Your function executed successfully!\",\"input\":{}}"
}

設定しておくと良いかも編

外部モジュールを1つのファイルにまとめないようにする

redis接続用のモジュールを使おうとインストールしたあと、デプロイコマンドを実行するとこんなエラーが起きました。

ERROR in ./node_modules/redis-parser/lib/hiredis.js
Module not found: Error: Can't resolve 'hiredis' in 'C:\Users\miyamoto.masanao\Documents\KITARO\GitHub\KITARO\server\sls-test\node_modules\redis-parser\lib'
 @ ./node_modules/redis-parser/lib/hiredis.js 3:14-32
 @ ./node_modules/redis-parser/lib/parser.js
 @ ./node_modules/redis-parser/index.js
 @ ./node_modules/redis/index.js
 @ ./handler.ts
 @ multi ./source-map-install.js ./handler.ts

どうもwebpackが外部モジュールも含めて1つのファイルにまとめようとするときに起きているエラーのようです。ということでnode_modulesの中のファイルはまとめないようにしてみます。

設定手順

webpack-node-externalsをインストールし、
webpack.config.jsに「★追加」コメントのある行を追加します。

# webpack-node-externalsをインストール
npm install webpack-node-externals -D
webpack.config.js
const path = require("path");
const slsw = require("serverless-webpack");
const nodeExternals = require("webpack-node-externals"); // ★追加
const entries = {};

Object.keys(slsw.lib.entries).forEach((key) => (entries[key] = ["./source-map-install.js", slsw.lib.entries[key]]));

module.exports = {
  /* 中略 */
  target: "node",
  externals: [nodeExternals()], // ★追加
  module: {
    rules: [
      // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader`
      { test: /\.tsx?$/, loader: "ts-loader" },
    ],
  },
};

package.json内にあるsource-map-supportの定義を
devDependenciesからdependenciesに移動させます。

package.json
  "dependencies": {
    "source-map-support": "^0.5.9" // ★devDependenciesから移動
  },
  "devDependencies": {
    "@types/aws-lambda": "8.10.1",
    "@types/node": "^8.0.57",
    "serverless-webpack": "^5.1.1",
    "ts-loader": "^4.2.0",
    "typescript": "^2.9.2",
    "webpack": "^4.5.0",
    "webpack-node-externals": "^1.7.2"
  },

serverless.yml に「★追加」コメントのある行を追加します。

serverless.yml
service:
  name: sls-test

# Add the serverless-webpack plugin
plugins:
  - serverless-webpack

provider:
  name: aws
  runtime: nodejs8.10

custom:                         # ★追加
  webpack: webpack.config.js    # ★追加
  webpackIncludeModules: true   # ★追加

# 以下略

これで外部モジュールがまとめられないようになります。

自前の共通モジュールをコンパイル対象にする

私はよくこういうフォルダ構成で開発をやっています。

/node_modules  # 外部モジュール。コンパイル対象にしたくない
/src
  ├ /node_modules # プロジェクト内で使う共有モジュール。コンパイル対象にしたい
  │  ├ /my-module1
  │  │  ├ hoge.ts
  │  │  └ piyo.ts
  │  └ /my-module2
  ├ /huga
  │  └ mogu.ts
  └ handler.ts

プロジェクト内のどこからでも使う共通処理があるとき、外部モジュールとは別の高さにあるnode_modulesの中に定義しておくと、読み込むときに階層を気にしなくていいので便利です。
上の例の場合ならhandler.tsからもmogu.tsからも、こんな感じで読み込めます。

import * as hoge from "my-module1/hoge";
import * as piyo from "my-module1/piyo";

ただ、serverless & TypeScript のデフォルト構成ではnode_modulesフォルダ内のコードはコンパイル対象にならないので、そのままではエラーになります。

ERROR in ./src/node_modules/my-module1/hoge.ts
Module build failed (from ./node_modules/ts-loader/index.js):
Error: Typescript emitted no output for *******\src\node_modules\my-module1\hoge.ts. By default, ts-loader will not compile .ts files in node_modules.
You should not need to recompile .ts files there, but if you really want to, use the allowTsInNodeModules option.
See: https://github.com/Microsoft/TypeScript/issues/12358
    at successLoader (*********\node_modules\ts-loader\dist\index.js:41:15)
    at Object.loader (*********\node_modules\ts-loader\dist\index.js:21:12)

設定手順

tsconfig.jsoninclude プロパティを追加し、コンパイル対象にしたいパスを指定します。

tsconfig.json
{
  "compilerOptions": {
    "sourceMap": true,
    "target": "es6",
    "lib": ["esnext"],
    "moduleResolution": "node"
  },
  "exclude": ["node_modules"],
  "include": [                     // ★追加
      "src/**/*",                  // ★追加
      "./src/node_modules/**/*"    // ★追加
   ]                               // ★追加
}

webpack.jsonallowTsInNodeModules オプションを追加します。

webpack.json
const path = require("path");
/* 中略 */

module.exports = {
  mode: slsw.lib.webpack.isLocal ? "development" : "production",
  /* 中略 */
  module: {
    rules: [
      // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader`
      { 
          test: /\.tsx?$/, 
          loader: "ts-loader", 
          options: { allowTsInNodeModules: true } // ★追加
      },
    ],
  },
};

以上です。他にも良いカスタマイズ方法が見つかれば追記していきます。