0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

環境

  • 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 を作ってくれる

この状態の課題

  • :warning: Typescript で開発したい
  • :warning: 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 コマンドでは設定できない
    • :arrow_down_small: Lambda cosole での設定画面 :arrow_down:
    • image.png
  • Lambda の console では、index.js の代わりとなるファイル名( => index.js )や実行する関数名 ( => handler ) は指定できるが、ファイルへのパス ( => ./src/* ) は指定できない. たぶん

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

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

この状態の課題

  • 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 を書き換える
// 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;

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

.
└── <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.jshandler が定義されるようにして、src/index.js ではその handler を import してそのまま export することで、実質的に src/dist/index.js をトリガー関数としているのである

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

  • tsconfig.json と src/index.js を工夫することで js が1つの場所にまとまった

この状態の課題

  • :warning: 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 近くあった

が条件となる。

そこで、

  1. esbuild によってバンドルし
  2. node_modules を削除し
  3. 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 などの処理を指定することができる。
直接的ではないが、絡んだことを解説している記事がこちら :arrow_double_down:

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

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

image.png

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

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

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

この状態の課題

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

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

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

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

が、

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

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

ところで

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

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

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

上記をカスタマイズする方法があれば教えてください!!!(切実)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?