Amplify Gen1 lambda で高い開発体験を目指した話

# 環境

- Amplify Gen1
- Amplify CLI: 12.12.6

# 目的

- Amplify Lambda を Typescript で開発したい！
- JS は１フォルダにまとめ、git 管理したくない！
- Lambda にアップロードする zip ファイルサイズを減らしてデプロイ時間を減らしたい！


# 道のり1. Amplify で最低限の Lambda を作成するまで

この道のりを説明した記事はたくさんあるので、説明は省略する

https://dev.classmethod.jp/articles/amplify-function-env-vars/

https://zenn.dev/sueyoshi/articles/5fc088818678c0

この道のりの果てに、以下のようなファイル構成となる

```bash
.
└── <project-root>/
    └── amplify/
        └── backend/
            └── function/
                └── <functionName>/
                    ├── <funcationName>-cloudformation-template.json
                    ├── custom-policies.json
                    ├── function-parameters.json
                    └── src/
                        ├── node_modules/    <- サイズでっかくなりがち
                        ├── event.json
                        ├── index.js
                        ├── sample-logic.js  <- 追加した
                        └── package.json
```

この道のりを簡潔に言えば

:::note info
Amplify CLI で対話的に頷けばテンプレート Lambda を作ってくれる
:::

この状態の課題

:::note warn
- :warning: Typescript で開発したい
- :warning: Lambda へ push する zip に node_modules が含まれてサイズがでっかい
:::

# 道のり2. Typescript で開発する

これもよくある道のりなので、所属する組織がたどった道のりに近い記事を載せておく

https://zenn.dev/pirosikick/scraps/a212e08443347e

`Amplify Lambda Typescript` で検索すると下記の記事が出てくるが、Gen2 なので別物である

https://aws.amazon.com/jp/blogs/news/amplify-functions-create-serverless-functions-using-typescript-powered-by-aws-lambda/

当時実際に組織で定義したもの

```jsonc
// tsconfig.json
{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "target": "es5",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "lib": ["dom", "esnext"],
    "module": "commonjs",
    "moduleResolution": "node",
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "outDir": "./",                 // <- ここが "./dist" とかではなく "./" なのは後述
    "baseUrl": "./",
    "rootDir": "./lib",
    "paths": {
      "src": ["./lib"]
    },
    "noImplicitAny": false
  },
  "include": ["./lib"],
  "exclude": []
}
```

結果フォルダ構成

```
.
└── <project-root>/
    └── amplify/
        └── backend/
            └── function/
                └── <functionName>/
                    └── <funcationName>-cloudformation-template.json/
                        ├── custom-policies.json
                        ├── function-parameters.json
                        └── src/
                            ├── lib/                   <- この中に ts のコード書く
                            │   ├── index.ts
                            │   ├── sample-logic.ts
                            │   └── client/
                            │       ├── dynamo.ts
                            │       └── index.ts
                            ├── node_modules/
                            ├── event.json
                            ├── index.js               <- そしたらここに js ファイルが出る
                            ├── sample-logic.js
                            ├── client/                <- サブフォルダも全部 js が root にで出てうざい
                            │   ├── dynamo.js
                            │   └── index.js
                            └── package.json
```

tsconfig.json にて compilerOptions.outDir を一般的な ./dist 等にしていない理由であるが、Amplify の Lambda のデフォルトロジックが下記のためである：

- Lambda のトリガーとなる関数は、function/src にある index.js ファイルが export している handler 関数
- このトリガー関数は Lambda の console 上から設定できるが、**Amplify CLI コマンドでは設定できない**
    - :arrow_down_small: Lambda cosole での設定画面 :arrow_down: 
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714399/5b7d8fd6-98af-c1bd-3ac2-7580f782eab2.png)
- Lambda の console では、index.js の代わりとなるファイル名( => index.js )や実行する関数名 ( => handler ) は指定できるが、**ファイルへのパス ( => `./src/*` ) は指定できない**. たぶん

この道のりを簡潔に言えば

:::note info
- ts でコード書いて
- tsc インストールして
- tsconfig.json 書けば ts が js に変換されるのでそれを push すればよい
:::

この状態の課題

:::note warn
- js ファイルが root に出てきてうざい
- :warning: Lambda へ push する zip に node_modules が含まれてサイズがでっかい
:::

# 道のり3. js ファイルを /dist へ引っ越す

道のり2 の

> Lambda の console では、index.js の代わりとなるファイル名( => index.js )や実行する関数名 ( => handler ) は指定できるが、**ファイルへのパス ( => `./src/` ) は指定できない**

という制約を守ったまま、回避した.

道のり2 の状態から 2 点変更する：

- tsconfig.json の outDir を変更する
- src/index.js を書き換える

```diff
// tsconfig.json
{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "target": "es5",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "lib": ["dom", "esnext"],
    "module": "commonjs",
    "moduleResolution": "node",
    "skipLibCheck": true,
    "resolveJsonModule": true,
-   "outDir": "./",                 
+   "outDir": "./dist",
    "baseUrl": "./",
    "rootDir": "./lib",
    "paths": {
      "src": ["./lib"]
    },
    "noImplicitAny": false
  },
  "include": ["./lib"],
  "exclude": []
}
```

```javascript
// src/index.js
const {handler} = require('./dist/index.js');
exports.handler = handler;
```

この結果、フォルダ構成 :arrow_double_down: 

```diff
.
└── <project-root>/
    └── amplify/
        └── backend/
            └── function/
                └── <functionName>/
                    └── <funcationName>-cloudformation-template.json/
                        ├── custom-policies.json
                        ├── function-parameters.json
                        └── src/
                            ├── lib/                   <- この中に ts のコード書く
                            │   ├── index.ts
                            │   ├── sample-logic.ts
                            │   └── client/
                            │       ├── dynamo.ts
                            │       └── index.ts
+                           ├── dist/
+                           │   ├── index.js
+                           │   ├── sample-logic.js
+                           │   └── client/
+                           │       ├── dynamo.js
+                           │       └── index.js
                            ├── node_modules/
                            ├── event.json
                            ├── index.js              <- この src/index.js は残す必要あり
-                           ├── sample-logic.js
-                           ├── client/                
-                           │   ├── dynamo.js
-                           │   └── index.js
                            └── package.json
```

これにより、`/dist` を `.gitignore` で指定することでスッキリするようになる

要は、`src/dist/index.js` に `handler` が定義されるようにして、`src/index.js` ではその `handler` を import してそのまま export することで、実質的に `src/dist/index.js` をトリガー関数としているのである

この道のりを簡潔に言えば

:::note info
- tsconfig.json と src/index.js を工夫することで js が１つの場所にまとまった
:::

この状態の課題

:::note warn
- :warning: Lambda へ push する zip に node_modules が含まれてサイズがでっかい
:::

# 道のり4. Lambda にデプロイするファイルサイズを小さくする

Lambda にデプロイするファイルサイズは小さい方がよい.

- Amplify push にかかる時間は短くなるし
- Lambda の コールドスタート の時間が減る

公式では、esbuild を用いて コールドスタートが 1.7倍 早くなったとのことである

https://aws.amazon.com/jp/blogs/news/optimizing-node-js-dependencies-in-aws-lambda

そこで、上記の公式に従って esbuild によってファイルサイズを小さくした

```diff
// pakcage.json
{
  "scripts": {
-   "build": "npx tsc -p tsconfig.json",
+   "build": "rm -rf dist && esbuild ./lib/index.ts --bundle --minify --platform=node --target=node18 --outdir=dist --log-limit=0", 
  },
  ...
}
```

これにより、src/dist/index.js に全ての js が圧縮される。

tsc だけでは ts ファイルを js へと変換しただけであるため、node_modules は必要となるが、esbuild によってバンドルした結果にはツリーシェイキング等で必要な node_modules のコードもすべて詰まっている.

これにより

- esbuild 前: node_modules + src/lib/** (js ファイル) => 約 300 MB (zip: 53MB)
- esbuild 後: src/lib/index.js => 約 10 MB (zip: 2MB)

と、S3 にアップロードする zip ファイルで比較すると 90% 以上ファイルサイズが小さくなった。

この結果、フォルダ構成は以下となる

```diff
.
└── <project-root>/
    └── amplify/
        └── backend/
            └── function/
                └── <functionName>/
                    └── <funcationName>-cloudformation-template.json/
                        ├── custom-policies.json
                        ├── function-parameters.json
                        └── src/
                            ├── lib/                   <- この中に ts のコード書く
                            │   ├── index.ts
                            │   ├── sample-logic.ts
                            │   └── client/
                            │       ├── dynamo.ts
                            │       └── index.ts
                            ├── dist/
                            │   ├── index.js          <- 全ての js と node_modules の成分が詰まっている
-                           │   ├── sample-logic.js
-                           │   └── client/
-                           │       ├── dynamo.js
-                           │       └── index.js
                            ├── node_modules/
                            ├── event.json
                            ├── index.js
                            ├── sample-logic.js
                            └── package.json
```

さて、これで ampligy push しておしまい、と言いたいところだが、Amplify Lambda は以下の仕様なのである

:::note info
/src 下のファイルを全て zip してデプロイ
:::

そのため、

:::note alert
せっかく src/dist/index.js に最適化・最小限の js ファイルがあるというのに、node_modules も zip する
:::

ことになり、**逆にファイルサイズが増えてしまう**

そのため、

- 「Amplify push する際、src/ に node_modules がないこと」
  - 当時、node_modules だけで 290 MB 近くあった

が条件となる。

そこで、

1. esbuild によってバンドルし
2. node_modules を削除し
3. amplify push する

ことで達成した.

1 のやり方は先述の通りで
2, 3 は、<root_dir>/package.json を書き換えた

```json
// <root_dir>/package.json
{
  "scripts": {
    "amplify:functionName": "cd amplify/backend/function/functionName/src && npm ci && npm run build && rm -rf node_modules",
  }
}
```

※ Amplify は <root_dir>/package.json の scripts によって、Amplify push 前の build などの処理を指定することができる。
直接的ではないが、絡んだことを解説している記事がこちら :arrow_double_down: 

https://qiita.com/tronicboy/items/a7b8c929dee5328c5a09#%E3%83%AB%E3%83%BC%E3%83%88%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E3%83%BC%E3%81%AEpackagejson%E3%81%ABscript%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B

この script により、node_modules を デプロイ せずに済み、結果的に S3 へ保存されたファイルが 2.7MB になっていた

一番上のものがこの 道のり4 を経過した結果. 55MB -> 2.8MB の大幅削減に成功した

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714399/bc22e41e-5818-3fc1-25eb-b3e649ab3e83.png)

ただし、１点悩みがあり、今のままでは local で amplify push した後、 <functionName>/src/node_modules が削除されてしまうので、push 後毎度 `npm i` 等しないといけない.

この道のりを簡潔に言えば

:::note info
- esbuild を利用して js をバンドルした
- Amplify push されるファイルから node_modules を削除してファイルサイズを激減させた
:::

この状態の課題

:::note warn
- :warning: local から Amplify push した際、node_modules を毎度インストールする必要がある
:::

# 道のり5. node_modules を毎度インストールせずに済むようにする

実はこの道のりはまだ達成していない

達成してないうちに年が変わりそうになってしまった

が、

1. push した後に `node_modules` を install するか
1. `node_modules` を一時的に退避させるか
1. `src/lib` に書いている ts ファイルや `node_modules` を別のフォルダ、例えば `/src/../lib`とかに書いて、`src` には生成物のみ含めるようにする

とすれば達成可能なので、そこまで難しくない. 3 は git 履歴荒らすので 1か2 かしら


# ところで

> Lambda の console では、index.js の代わりとなるファイル名( => index.js )や実行する関数名 ( => handler ) は指定できるが、**ファイルへのパス ( => `./src/` ) は指定できない**

> /src 下のファイルを全て zip してデプロイ

という２つの Amplify の仕様という制約の中で戦ってきたわけだが

:::note info
上記をカスタマイズする方法があれば教えてください！！！（切実）
:::