環境
- Amplify Gen1
- Amplify CLI: 12.12.6
目的
- Amplify Lambda を Typescript で開発したい!
- JS は1フォルダにまとめ、git 管理したくない!
- Lambda にアップロードする zip ファイルサイズを減らしてデプロイ時間を減らしたい!
道のり1. Amplify で最低限の Lambda を作成するまで
この道のりを説明した記事はたくさんあるので、説明は省略する
この道のりの果てに、以下のようなファイル構成となる
.
└── <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
この道のりを簡潔に言えば
Amplify CLI で対話的に頷けばテンプレート Lambda を作ってくれる
この状態の課題
- Typescript で開発したい
- Lambda へ push する zip に node_modules が含まれてサイズがでっかい
道のり2. Typescript で開発する
これもよくある道のりなので、所属する組織がたどった道のりに近い記事を載せておく
Amplify Lambda Typescript
で検索すると下記の記事が出てくるが、Gen2 なので別物である
当時実際に組織で定義したもの
// 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 コマンドでは設定できない
- Lambda の console では、index.js の代わりとなるファイル名( => index.js )や実行する関数名 ( => handler ) は指定できるが、ファイルへのパス ( =>
./src/*
) は指定できない. たぶん
この道のりを簡潔に言えば
- ts でコード書いて
- tsc インストールして
- tsconfig.json 書けば ts が js に変換されるのでそれを push すればよい
この状態の課題
- js ファイルが root に出てきてうざい
- 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 を書き換える
// 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": []
}
// src/index.js
const {handler} = require('./dist/index.js');
exports.handler = handler;
この結果、フォルダ構成
.
└── <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
をトリガー関数としているのである
この道のりを簡潔に言えば
- tsconfig.json と src/index.js を工夫することで js が1つの場所にまとまった
この状態の課題
- Lambda へ push する zip に node_modules が含まれてサイズがでっかい
道のり4. Lambda にデプロイするファイルサイズを小さくする
Lambda にデプロイするファイルサイズは小さい方がよい.
- Amplify push にかかる時間は短くなるし
- Lambda の コールドスタート の時間が減る
公式では、esbuild を用いて コールドスタートが 1.7倍 早くなったとのことである
そこで、上記の公式に従って esbuild によってファイルサイズを小さくした
// 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% 以上ファイルサイズが小さくなった。
この結果、フォルダ構成は以下となる
.
└── <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 は以下の仕様なのである
/src 下のファイルを全て zip してデプロイ
そのため、
せっかく src/dist/index.js に最適化・最小限の js ファイルがあるというのに、node_modules も zip する
ことになり、逆にファイルサイズが増えてしまう
そのため、
- 「Amplify push する際、src/ に node_modules がないこと」
- 当時、node_modules だけで 290 MB 近くあった
が条件となる。
そこで、
- esbuild によってバンドルし
- node_modules を削除し
- amplify push する
ことで達成した.
1 のやり方は先述の通りで
2, 3 は、/package.json を書き換えた
// <root_dir>/package.json
{
"scripts": {
"amplify:functionName": "cd amplify/backend/function/functionName/src && npm ci && npm run build && rm -rf node_modules",
}
}
※ Amplify は /package.json の scripts によって、Amplify push 前の build などの処理を指定することができる。
直接的ではないが、絡んだことを解説している記事がこちら
この script により、node_modules を デプロイ せずに済み、結果的に S3 へ保存されたファイルが 2.7MB になっていた
一番上のものがこの 道のり4 を経過した結果. 55MB -> 2.8MB の大幅削減に成功した
ただし、1点悩みがあり、今のままでは local で amplify push した後、 /src/node_modules が削除されてしまうので、push 後毎度 npm i
等しないといけない.
この道のりを簡潔に言えば
- esbuild を利用して js をバンドルした
- Amplify push されるファイルから node_modules を削除してファイルサイズを激減させた
この状態の課題
- local から Amplify push した際、node_modules を毎度インストールする必要がある
道のり5. node_modules を毎度インストールせずに済むようにする
実はこの道のりはまだ達成していない
達成してないうちに年が変わりそうになってしまった
が、
- push した後に
node_modules
を install するか -
node_modules
を一時的に退避させるか -
src/lib
に書いている ts ファイルやnode_modules
を別のフォルダ、例えば/src/../lib
とかに書いて、src
には生成物のみ含めるようにする
とすれば達成可能なので、そこまで難しくない. 3 は git 履歴荒らすので 1か2 かしら
ところで
Lambda の console では、index.js の代わりとなるファイル名( => index.js )や実行する関数名 ( => handler ) は指定できるが、ファイルへのパス ( =>
./src/
) は指定できない
/src 下のファイルを全て zip してデプロイ
という2つの Amplify の仕様という制約の中で戦ってきたわけだが
上記をカスタマイズする方法があれば教えてください!!!(切実)