TypeScriptによるServerless Frameworkの利用

# はじめに

- Serverless FrameworkをTypeScriptテンプレート(aws-nodejs-typescript)で利用した情報があまり見当たらなかったので、プロジェクト作成から設定内容、ファイル構成なんかについて記載します。
- 今後利用する方の参考になればと。

# プロジェクト作成

- Serverless Frameworkをインストールした後にプロジェクトを作成します。
    - テンプレートにaws-nodejs-typescriptを利用します。

```bash
$ sls create --template aws-nodejs-typescript --path serverless-typescript-sample
```
- [こちら](https://github.com/serverless/serverless/tree/master/lib/plugins/create/templates/aws-nodejs-typescript)にある内容でプロジェクトが作成されます。
  - README.mdファイルに詳細が記載されているので確認しておくと良いです。

# 環境の整理
## 静的コード解析ツールの追加

- プロジェクト作成後の状態だとeslintなどのコード解析ツールが入っていないので、適宜追加します。
  - 細かい設定内容はお好みで設定して頂ければと。

```bash
$ yarn add -D eslint prettier eslint-config-prettier eslint-plugin-prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser

```
- .eslintrc.jsと.prettierrc.jsファイルを追加して設定します。

## middyのバージョンが古いので更新する

- slsで生成されるコードはmiddyパッケージのバージョンが古いのでアップデートしておきます。

```bash
$ yarn upgrade @middy/core --latest
$ yarn upgrade @middy/http-json-body-parser --latest
```

# 型定義の修正

## eslintの実行

- ここでeslintを実行するとフォーマットエラーなどでエラーがいくつか出力されるので、 `--fix`で適宜修正しておきます。

```bash
$ eslint "{src,test}/**/*.ts"
```

- 型の指定でいくつかworningが出るので適宜修正します。
- 現時点で出たものは以下になります。

### src/libs/handlerResolver.ts

- handlerPathの戻り値の型が指定されていないので、stringを指定するようにします。

```ts:handlerResolver.ts
export const handlerPath = (context: string): string => {
  return `${context.split(process.cwd())[1].substring(1).replace(/\\/g, '/')}`
}
```


### src/libs/apiGateway.ts

- formatJSONResponseの戻り値の型が指定されていないので追加しておきます。

```ts:apiGateway.ts
type JSONResponse = {
  statusCode: number
  body: string
}

export const formatJSONResponse = (
  response: Record<string, unknown>,
): JSONResponse => {
  return {
    statusCode: 200,
    body: JSON.stringify(response),
  }
}
```

### src/libs/lambda.ts

- middyfyの引数と戻り値の型が指定されていないので、追加しておきます。

```ts:lambda.ts
import type { Handler } from 'aws-lambda'

export const middyfy = (handler: Handler): middy.MiddyfiedHandler => {
  return middy(handler).use(middyJsonBodyParser())
}
```
## 型定義ファイル

- 利用しているパッケージなどの型定義ファイルは以下から参照出来ます。
- serverless.tsで定義する型
  - https://github.com/serverless/typescript/blob/master/index.d.ts
- middlewareパッケージのMiddyで利用する型(coreとhttp-json-body-parser)
  - coreパッケージ
        - https://github.com/middyjs/middy/blob/main/packages/core/index.d.ts
  - http-json-body-parser
        - https://github.com/middyjs/middy/blob/main/packages/http-json-body-parser/index.d.ts
- aws-lambdaで利用する型
  - https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/aws-lambda
- aws-sdk
  - プロジェクトを作成した際には含まれていませんが、aws-sdkを利用する場合にはこのあたりを参考に。
    - https://github.com/aws/aws-sdk-js#usage-with-typescript

# ローカル実行

- この時点で一度ローカル実行してみます。
- 実行しやすいように以下のパッケージを事前にインストールしておきます。

## serverless-offline

- [serverless-offline](https://github.com/dherault/serverless-offline)
- ローカルでAPI Gateway + Lambdaをエミュレーションしてくれます。

```bash
$ yarn add -D serverless-offline
```
- serverless.tsにプラグインとして追加します。

```ts:serverless.ts
plugins: ['serverless-webpack', 'serverless-offline'],
```

## fork-ts-checker-webpack-plugin

- [fork-ts-checker-webpack-plugin](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin)
- webpack実行時にESLintで型チェックをしてくれます。
  - serverless-offline実行時にあわせて型のチェックも出来るようになります。

```bash
yarn add -D fork-ts-checker-webpack-plugin
```

- webpack.config.jsにプラグインとして追加します。
- require定義の追加

```js:webpack.config.js
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
```
- plugins定義の追加

```js:webpack.config.js
  plugins: [
    new ForkTsCheckerWebpackPlugin({
      eslint: {
        files: './src/**/*.{ts,tsx,js,jsx}',
      },
    }),
  ],
```

## 実行

- serverless-offlineを利用してプロセスを起動します。

```bash
$ sls offline
```

- curlなどでリクエストします。

```bash
curl --request POST \
  --url http://localhost:3000/dev/hello \
  --header 'Content-Type: application/json' \
  --data '{
	"name": "hoge"
}'
```

- 正しく処理されればOKです。

# ファイル構成について

- プロジェクト生成後のファイル構成についての詳細は[こちら](https://github.com/serverless/serverless/blob/master/lib/plugins/create/templates/aws-nodejs-typescript/README.md)に記載されています。
- 基本的にはfunctions/に必要なLambda-functionの処理を追加してserverless.tsに設定を追加してゆく感じになります。
- 構成についておおまかですが、下記に記載します。

## src/libs/lambda.ts

- middlewareパッケージとして[Middy](https://github.com/middyjs/middy)を利用する構成になっています。
  - メインの処理を行なう前、後に共通して実行したい処理がある場合にはこちらを利用して追加してゆく感じになります。
    - デフォルトでは[http-json-body-parser](https://github.com/middyjs/middy/blob/main/packages/http-json-body-parser/README.md)を利用しています。
      - Content-Typeにapplication/jsonを指定してリクエストすることで、リクエストボディに指定したjsonをパースしてくれます。
    - [こちら](https://github.com/middyjs/middy/tree/main/packages)によく利用しそうな処理が用意されているので、それを利用するか、独自に実装することも可能です。
    - アプリケーション全体として必要な初期化処理、DBの接続処理やエラー処理などを実装するのが良いかもしれません。

## src/libs/apiGateway.ts

- httpリクエストとレスポンスの型の定義などがされています。
- リクエストボディのjsonの型定義には[json-schema-to-ts](https://github.com/ThomasAribart/json-schema-to-ts)というパッケージが利用されていて、JSON schemaで定義したものをそのままバリデーションなどの型定義として利用出来るようになっています。
  - src/functions/hello/schema.tsでリクエストボディの型が定義されています。

## src/functions/handler.ts

- メインの処理になります。
- デフォルトであるhelloは[こちら](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/handler.d.ts#L84)のHandler型が定義されていて、src/functions/hello/schema.tsで定義されたデータがevent.bodyに入ってくるようになっています。
  - eventで渡されるデータはbody以外は[APIGatewayProxyEvent](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/api-gateway-proxy.d.ts)の定義にある通りになります。

# その他

## リクエストデータの変更

- リクエストボディの定義を変更する場合にはsrc/functions/hello/schema.tsファイルの定義を変更します。
- ageという項目を必須として追加したい場合なこのような感じになります。

```ts:schema.ts
export default {
  type: 'object',
  properties: {
    name: { type: 'string' },
    age: { type: 'number' },
  },
  required: ['name', 'age'],
} as const
```


## 環境ごとの設定

- TypeScript特有ではありませんが、デプロイ環境ごとに環境変数を設定する方法です。
- serverless.tsに設定を追加します。
- デプロイ時のstageオプションで環境を取得するように設定して、環境ごとの設定ファイルを追加します。

```ts:serverless.ts
  provider: {
    stage: '${opt:stage, self:custom.defaultStage}',
```

- 設定ファイルの追加

```bash
env/
  dev.yml
  prod.yml
  stg.yml
```

- ファイルの中身

```yaml:dev.yml
ENV: 'development'
```

- custom設定にdefaultStageとenvironmentを追加
  - `./env/**.yml`ファイルを追加してそこから値を読み込むようにする

```ts:serverless.ts
  custom: {
    defaultStage: 'dev',
    environment: {
      dev: '${file(./env/dev.yml)}',
      stg: '${file(./env/stg.yml)}',
      prod: '${file(./env/prod.yml)}',
    },
```

- environmentの設定で、環境変数を設定する

```ts:serverless.ts
    environment: {
      AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
      ENV: '${self:custom.environment.${self:provider.stage}.ENV}',
    },
```

## AWSリソースの設定 

- serverless.tsに設定を追加します。
- iamロール
  - providerのiamRoleStatementsに設定を追加
    - Fn::Join などの処理も追加することが出来ます
  - s3の特定のバケットへの操作を許可するように設定する例になります。

```ts:serverless.ts
    iamRoleStatements: [
      {
        Effect: 'Allow',
        Action: ['s3:*'],
        Resource: [
          {
            'Fn::Join': [
              '',
              [
                'arn:aws:s3:::',
                '${self:custom.environment.${self:provider.stage}.BUCKET_NAME}',
                '/*',
              ],
            ],
          },
          {
            'Fn::Join': [
              '',
              [
                'arn:aws:s3:::',
                '${self:custom.environment.${self:provider.stage}.BUCKET_NAME}',
              ],
            ],
          },
        ],
      }
    ]
```

- リソース
  - resourcesに作成するAWSリソースを追加します。
  - s3バケット作成の例になります。

```ts:serverless.ts
  resources: {
    Resources: {
      Bucket: {
        Type: 'AWS::S3::Bucket',
        Properties: {
          BucketName:
            '${self:custom.environment.${self:provider.stage}.BUCKET_NAME}',
          VersioningConfiguration: {
            Status: 'Enabled',
          },
        },
      },
    },
```

# まとめ

- js(node)で実装するのと大きな違いはありませんが、いくつか外部のパッケージを利用する構成になっているので、そこだけ気をつければ問題は無いと思います。
- aws-sdkなど外部のパッケージを利用して実装してゆくと型定義をそれぞれ行なう必要があり、若干の面倒くささはありますが、jsだけで実装しているとオブジェクトの中身も良く分からずに利用してしまっていたりするので、安全に実行できますしメンテナンス性もあがるかな、と感じます。
- 上記の内容で作成したサンプルを[こちら](https://github.com/myoshioka/serverless-typescript-sample)にアップしてあります。