AWS Lambda Advent Calendar 13日目の記事です。
今回は AWS Lambda function で TypeScript の 関数を書くときにコンソール上で2000倍(※当社比)見栄え良くする方法を書きます。
ちなみにここでの"見栄えの良さ"はAWS コンソール上での読みさすさのことです! 副次的な効果として、最初のロード時のコードサイズが限界まで小さくなるので、パフォーマンスも良くなるかもしれません。(未検証)
なお、これはAWS CDKを使うことが前提となっています。
通常の場合
まずは比較対象として、何もせずにTypeScriptでLambda functionを書いてデプロイしてみます。コードはこちらです。
https://github.com/tomoima525/clean-nodejs-lambda
TypeScriptを使う場合、 aws_lambda_nodejs.NodejsFunction
を使ってデプロイするのが一般的です。
new lambda_nodejs.NodejsFunction(this, "simple-llm", { runtime: lambda.Runtime.NODEJS_18_X, handler: "handler", entry: path.join(`${__dirname}/../`, "functions", "simple-llm/index.ts"), });
今回は、今はやりのLLMフレームワークである Langchainを使って簡単な応答を返す関数を書きました。
import { OpenAI } from "langchain/llms/openai"; export const handler = async (event: any = {}): Promise<any> => { console.log("request:", JSON.stringify(event, undefined, 2)); const llm = new OpenAI({ temperature: 0.9, }); const text = "What would be a good company name for a company that makes colorful socks?"; const llmResult = await llm.predict(text); console.log("=====", llmResult); };
これをデプロイした結果が以下です。
何もしないとLambda functionのサイズは 509 kb、行数はおよそ 32000行となります。スクショを見ても明らかな通り、大変可読性が低いですね。
NodejsFunction
は内部的に esbuildを利用しているため、全てのライブラリをバンドルしてひとつのindex.jsを生成します。そのためこのようにバカでかいファイルが生成されてしまうのです。
それでは2つのステップを通じてこれを読みやすくしてみましょう。
見栄え良く書くステップ1 - Lambda Layer
最初のステップは、lambda layerを活用することです。”当たり前じゃん!” と思う人もいるかもしれませんが、よく紹介される /opt/
配下にライブラリを置く方法(例 https://www.cloudtechsimplified.com/aws-lambda-layers/ ) だと、esbuildによってすべてのコードがバンドルされてしまうため、ファイルサイズは減らず、可読性はあがりません。ここでは nodejs
配下にlayerのファイルを置く方法でバンドルファイルに含まない方法を取ります。
まず トップディレクトリに layers/llm-layer/nodejs
というディレクトリを作ります。
次に nodejs
以下で npm init
します。ここがポイントで、プロジェクトがパッケージマネジャーに pnpm
を使おうが yarn
を使おうが、npmを使ってください。なぜかというと、 pnpm
や yarn
はキャッシュを利用するため、その分 node_modules
のサイズが大きくなってしまうからです。
そして必要なライブラリ(今回は langchain )をインストールします。
次に LLM用のLayerを追加します。
const llmLayer = new lambda.LayerVersion(this, "llmLayer", { compatibleRuntimes: [lambda.Runtime.NODEJS_18_X], code: lambda.Code.fromAsset( path.join(`${__dirname}/../`, "layers/llm-layer"), ), description: "Langchain LLM Layer", });
そしてこのLayerを最初の Lambda functionに追加します。もう一点、レイヤーのライブラリを利用するため、 externalModules
を指定します
new lambda_nodejs.NodejsFunction(this, "simple-llm", { runtime: lambda.Runtime.NODEJS_18_X, handler: "handler", entry: path.join(`${__dirname}/../`, "functions", "simple-llm/index.ts"), layers: [llmLayer], bundling: { // layerに含まれるライブラリはbundleから省く externalModules: ["langchain"], }, });
注意点として、Lambda functionに含まれるライブラリのバージョンとlayerに含まれるライブラリのバージョンをあわせることがあります。ローカル開発時ではLambda functionに含まれるライブラリを参照するので問題ないですが、デプロイ後はLayerのライブラリを参照することになるので、場合によっては動かなくなることもあります。
では、これを再度デプロイしてみます。
関数のサイズは 852b、コード数は 35行 まで削減できました!
見栄え良く書くステップ2 - ESM
ステップ1だけでもだいぶ見やすくなったのですが、まだ前半の commonjs のコードが気になりますね。そこで登場するのが ESM(ECMA Script Module) です。実は Lambda function はしばらく前から ESM がサポートされています。これを有効化すると、commonjs の変換コードが不要になるわけです。以下をNodejsFunctionに追加します。
new lambda_nodejs.NodejsFunction(this, "simple-llm", { runtime: lambda.Runtime.NODEJS_18_X, handler: "handler", entry: path.join(`${__dirname}/../`, "functions", "simple-llm/index.ts"), layers: [llmLayer], bundling: { // layerに含まれるライブラリ externalModules: ["langchain"], format: lambda_nodejs.OutputFormat.ESM, }, });
そして ESMをpackageでも有効化します。
{ "name": "simple-llm", "packageManager": "yarn@3.2.1", "dependencies": { "langchain": "^0.0.206" }, "type": "module" }
デプロイしてみます。
サイズは442b、コードはわずか15行まで削減されました。
元のコードと比較すると 2140倍分もコードが削減されたことになります! めちゃくちゃ見やすいしコンソール上でちょっといじったりもしやすい!!
最終的なコードはこちらです。
https://github.com/tomoima525/clean-nodejs-lambda/tree/clean-typescript
小ネタ: ESMに対応してないライブラリについて
Lambda functionはESMに対応しているのに、非常に残念ながら一部の aws-sdkのライブラリ(例 secret-manager-sdk)は対応対応しておらず、Lambda functionに含むとランタイムエラーを起こします。
{ "errorType": "Error", "errorMessage": "Dynamic require of \"crypto\" is not supported", "stack": [ "Error: Dynamic require of \"crypto\" is not supported", " at file:///var/task/index.mjs:12:9", ...
この回避策としては bundleにエントリーポイントを追加することと、ESM シンタックスのサポートを加えてあげる方法があります。
bundling: { format: OutputFormat.ESM, mainFields: ['module', 'main'], // moduleを先にする target: 'esnext', // ESMでCommonjsを読めるようにする banner: "import { fileURLToPath } from 'url'; import { createRequire } from 'module';const require = createRequire(import.meta.url); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename);", }
まとめ
この記事ではAWSコンソール上で 32000 行 あったTypeScriptベースの Lambda functionを 15 行まで減らし(※なのでおおよそ2000倍分 ^^;)、可読性を劇的に上げる方法を紹介しました。"2000倍"ってケースバイケースやんというツッコミは年末ということでご容赦ください。ESMはTypeScriptに記法が近いので、積極的に切り替えて行きたいところです。