From 5a949ee66dfbd2e945e5cf433b884469c25e285d Mon Sep 17 00:00:00 2001 From: miyaji255 <84168445+miyaji255@users.noreply.github.com> Date: Thu, 28 Nov 2024 14:50:34 +0900 Subject: [PATCH 1/5] Add blog esbuild-lambda-layer --- .../blogs/2024-12-01-esbuild-lambda-layer.md | 375 ++++++++++++++++++ src/content/tags/aws-cdk.json | 16 + src/content/tags/aws-cdk.png | Bin 0 -> 936 bytes src/content/tags/aws-lambda.json | 12 + src/content/tags/aws-lambda.svg | 19 + src/content/tags/aws.json | 11 + src/content/tags/aws.svg | 39 ++ 7 files changed, 472 insertions(+) create mode 100644 src/content/blogs/2024-12-01-esbuild-lambda-layer.md create mode 100644 src/content/tags/aws-cdk.json create mode 100644 src/content/tags/aws-cdk.png create mode 100644 src/content/tags/aws-lambda.json create mode 100644 src/content/tags/aws-lambda.svg create mode 100644 src/content/tags/aws.json create mode 100644 src/content/tags/aws.svg diff --git a/src/content/blogs/2024-12-01-esbuild-lambda-layer.md b/src/content/blogs/2024-12-01-esbuild-lambda-layer.md new file mode 100644 index 0000000..82dfc79 --- /dev/null +++ b/src/content/blogs/2024-12-01-esbuild-lambda-layer.md @@ -0,0 +1,375 @@ +--- +title: esbuildで効率の良いLambda Layerを作る +description: "esbuildを使ってLambdaとLambda Layer用のバンドル方法をCDKを交えて説明します。" +category: tech +author: miyaji +tags: ["esbuild", "javascript", "aws", "aws-lambda", "aws-cdk"] +--- + +CDKでLambdaには`NodejsFunction`というバンドルとminifyを行うConstructがあるものの、Lambda Layerにはそのようなものがありません。そこで、esbuildを使ってLambdaとLambda Layerの両方のバンドルを行なっていきます。 + +## esbuildのCode Splitting + +esbuildはCode Splittingという複数のエントリーポイント間で共有されるコードを別の共有ファイルに分割する機能があります。具体的には以下のようなファイルが有るとき次のようにバンドルされます。 + +**バンドル前** +```javascript +// a.js +import { shared1 } from './shared1.js'; +import { shared2 } from './shared2.js'; + +export function a() { + console.log('a'); + shared1(); + shared2(); +} + +// b.js +import { shared1 } from './shared1.js'; +import { shared2 } from './shared2.js'; + +export function b() { + console.log('b'); + shared1(); + shared2(); +} + +// shared1.js +export function shared1() { + console.log('shared1'); +} + +// shared2.js +export function shared2() { + console.log('shared2'); +} +``` + +**バンドル後** +```javascript +// a.js +import { shared1, shared2 } from './chunk-1.js'; +function a() { + console.log('a'); + shared1(); + shared2(); +} + +// b.js +import { shared1, shared2 } from './chunk-1.js'; +function b() { + console.log('b'); + shared1(); + shared2(); +} + +// chunk-1.js +export function shared1() { + console.log('shared1'); +} + +export function shared2() { + console.log('shared2'); +} +``` + +この機能を使って共有部分を切り出してLambda Layerを作ります。 + + +## パッケージ化する + +LambdaとLambda Layerは次のような位置に配置されます + +- Lambda: `/var/task` +- Lambda Layer: `/opt` + +また、`NODE_PATH`環境変数には次のような値が含まれます。 + +- `/opt/nodejs/node_modules` +- `/opt/nodejs/node16/node_modules` +- `/opt/nodejs/node18/node_modules` +- `/opt/nodejs/node20/node_modules` + +したがって、Lambda Layerには`/opt/nodejs/node_modules`にパッケージを配置する必要があります。 +Lambda Layerを参照する際はパッケージ名を用いることを前提とした設計になっていますが、esbuildのCode Splittingは相対パスで出力されるため、インポートパスをビルド後に書き換えることで対応します。 + +## ビルドスクリプトの作成 + +以上をまとめてビルドスクリプトを作成します。処理としては以下の通りです。 + +1. ビルドする +2. インポートパスを書き換える +3. Lambda Layer用のpackage.jsonを作成する + +```javascript +import { build } from 'esbuild'; +import { readdir, writeFile, readFile } from 'node:fs/promises'; + +// ビルドする +await build({ + entryPoints: ["src/lambda/handler1.js", "src/lambda/handler2.js"], + bundle: true, + splitting: true, + minify: true, + format: "esm", + platform: "node", + target: "node20", + outdir: "dist", + outExtension: { ".js": ".mjs" }, + entryNames: "handlers/[name]", + chunkNames: "layers/nodejs/node_modules/layers/[name]-[hash]", +}); + +// Lambda Layerに対するimport pathを書き換える +const chunks = await readdir("dist/layers/nodejs/node_modules/layers"); +const searchValue = new RegExp( + `(?<=from\\s*")(\\./|\\.\\./)*layers/nodejs/node_modules/layers(?=/(${chunks.map((f) => f.replaceAll(".", "\\.")).join("|")})")`, + "g" +); + +const handlers = await readdir("dist/handlers"); +await Promise.all( + handlers.map(async (handler) => { + const content = await readFile(`dist/handlers/${handler}`, "utf-8"); + await writeFile( + `dist/handlers/${handler}`, + content.replaceAll(searchValue, "layers") + ); + }) +); + +// Lambda Layer用のpackage.jsonを作成する +await writeFile( + "dist/layers/nodejs/node_modules/layers/package.json", + JSON.stringify({ + name: "layers", + version: "1.0.0", + type: "module", + }) +); +``` + +## 更に効率化する + +ここまでで十分な効率化が行えますが、更に効率化するためには以下のような手法があります。 + +### aws-sdkの除外 + +Node.jsのLambdaにはaws-sdkが含まれているため、バンドル結果に含める必要はありません。esbuildの`external`オプションを使って除外します。 + +```diff + await build({ + entryPoints: ["src/lambda/handler1.js", "src/lambda/handler2.js"], + bundle: true, + splitting: true, + minify: true, + format: "esm", + platform: "node", + target: "node20", + outdir: "dist", + outExtension: { ".js": ".mjs" }, + entryNames: "handlers/[name]", + chunkNames: "layers/nodejs/node_modules/layers/[name]-[hash]", ++ external: ["@aws-sdk/*"], + }); +``` + +### `node_modules`をsourcemapから除外 + +エラー出力をわかりやすくするためにsourcemapを含めることがありますが、sourcemapは非常に大きいです。`node_modules`を除外することでsourcemapのサイズを削減します。 + +```diff ++ /** @type {import("esbuild").Plugin} */ ++ const excludeNodeModulesFromSourceMapPlugin = { ++ name: "excludeNodeModulesFromSourceMapPlugin", ++ setup(build) { ++ const emptySourceMapAsBase64 = Buffer.from(JSON.stringify({ version: 3, sources: [""], mappings: "A" })).toString( ++ "base64" ++ ); ++ build.onLoad({ filter: /node_modules.+\.(js|mjs|cjs|ts|mts|cts)$/ }, async (args) => { ++ return { ++ contents: `${await readFile(args.path, "utf8")}\n//# sourceMappingURL=data:application/json;base64,${emptySourceMapAsBase64}`, ++ loader: "default", ++ }; ++ }); ++ }, ++ }; + + await build({ + entryPoints: ["src/lambda/handler1.js", "src/lambda/handler2.js"], + bundle: true, + splitting: true, + minify: true, + format: "esm", + platform: "node", + target: "node20", + outdir: "dist", + outExtension: { ".js": ".mjs" }, + entryNames: "handlers/[name]", + chunkNames: "layers/nodejs/node_modules/layers/[name]-[hash]", + external: ["@aws-sdk/*"], ++ sourcemap: "inline", ++ sourceContent: false, ++ plugins: [excludeNodeModulesFromSourceMapPlugin], + }); +``` + +ここで使用した`excludeNodeModulesFromSourceMapPlugin`はesbuildのissueのコメントにあるものを使用させていただきました。 +https://github.com/evanw/esbuild/issues/1685#issuecomment-944928069 + +### defineを使って環境変数を埋め込む + +esbuildでは`define`を使ってシンボルに値を埋め込むことができます。これにより、falseであると確定されたif分岐は削除されます。 + +```diff + await build({ + entryPoints: ["src/lambda/handler1.js", "src/lambda/handler2.js"], + bundle: true, + splitting: true, + minify: true, + format: "esm", + platform: "node", + target: "node20", + outdir: "dist", + outExtension: { ".js": ".mjs" }, + entryNames: "handlers/[name]", + chunkNames: "layers/nodejs/node_modules/layers/[name]-[hash]", + external: ["@aws-sdk/*"], + sourcemap: "inline", + sourceContent: false, + plugins: [excludeNodeModulesFromSourceMapPlugin], ++ define: { ++ "process.env.NODE_ENV": '"production"', ++ }, + }); +``` + +他にもesbuildには最適化のオプションがありますので、必要に応じて使ってみてください。 +https://esbuild.github.io/api/#optimization + +## CDK + +以上のビルドスクリプトで作成したファイルをCode AssetとしてCDKでLambdaとLambda Layerを作成します。 + +ポイントとしてはassetから指定したhandler以外を除外することです。これにより、Lambdaには必要なファイルのみが含まれるため、Lambdaのサイズを小さくすることができます。 + +```typescript +const lambdaLayer = new lambda.LayerVersion(this, "LambdaLayer", { + code: lambda.Code.fromAsset("dist/layers"), + compatibleRuntimes: [lambda.Runtime.NODEJS_20_X], +}); + +const handlers = readdirSync("dist/handlers"); + +const handler1 = new lambda.NodejsFunction(this, "Handler1", { + handler: "handler1.handler", + code: lambda.Code.fromAsset("dist/handlers", { + exclude: handlers.filter((handler) => handler !== "handler1.mjs").map(handler => `**/${handler}`), + }), + layers: [lambdaLayer], + runtime: lambda.Runtime.NODEJS_20_X, +}); + +const handler2 = new lambda.NodejsFunction(this, "Handler2", { + handler: "handler2.handler", + code: lambda.Code.fromAsset("dist/handlers", { + exclude: handlers.filter((handler) => handler !== "handler2.mjs").map(handler => `**/${handler}`), + }), + layers: [lambdaLayer], + runtime: lambda.Runtime.NODEJS_20_X, +}); +``` + +## まとめ + +これで効率の良いLambda Layerを作成することができました。 + +最後に完成したビルドスクリプトをまとめておきます。 + +
+ビルドスクリプト + +```javascript +import { build } from 'esbuild'; +import { readdir, writeFile, readFile } from 'node:fs/promises'; + +import { build } from 'esbuild'; +import { readdir, writeFile, readFile } from 'node:fs/promises'; + +/** @type {import("esbuild").Plugin} */ +const excludeNodeModulesFromSourceMapPlugin = { + name: "excludeNodeModulesFromSourceMapPlugin", + setup(build) { + const emptySourceMapAsBase64 = Buffer.from(JSON.stringify({ version: 3, sources: [""], mappings: "A" })).toString( + "base64" + ); + build.onLoad({ filter: /node_modules.+\.(js|mjs|cjs|ts|mts|cts)$/ }, async (args) => { + return { + contents: `${await readFile(args.path, "utf8")}\n//# sourceMappingURL=data:application/json;base64,${emptySourceMapAsBase64}`, + loader: "default", + }; + }); + }, +}; + + +// ビルドする +await build({ + entryPoints: ["src/lambda/handler1.js", "src/lambda/handler2.js"], + bundle: true, + splitting: true, + minify: true, + format: "esm", + platform: "node", + target: "node20", + outdir: "dist", + outExtension: { ".js": ".mjs" }, + entryNames: "handlers/[name]", + chunkNames: "layers/nodejs/node_modules/layers/[name]-[hash]", external: ["@aws-sdk/*"], + sourcemap: "inline", + sourceContent: false, + plugins: [excludeNodeModulesFromSourceMapPlugin], + define: { + "process.env.NODE_ENV": '"production"', + }, +}); + +// Lambda Layerに対するimport pathを書き換える +const chunks = await readdir("dist/layers/nodejs/node_modules/layers"); +const searchValue = new RegExp( + `(?<=from\\s*")(\\./|\\.\\./)*layers/nodejs/node_modules/layers(?=/(${chunks.map((f) => f.replaceAll(".", "\\.")).join("|")})")`, + "g" +); + +const handlers = await readdir("dist/handlers"); +await Promise.all( + handlers.map(async (handler) => { + const content = await readFile(`dist/handlers/${handler}`, "utf-8"); + await writeFile( + `dist/handlers/${handler}`, + content.replaceAll(searchValue, "layers") + ); + }) +); + +// Lambda Layer用のpackage.jsonを作成する +await writeFile( + "dist/layers/nodejs/node_modules/layers/package.json", + JSON.stringify({ + name: "layers", + version: "1.0.0", + type: "module", + }) +); +``` + +
+ +## 参考資料 + +- esbuild 最適化芸人 (TATUSNO Yasuhiro) - Speaker Deck + https://speakerdeck.com/exoego/esbuild-zui-shi-hua-yun-ren +- Node.jsのLambdaコールドスタートで熱を上げる方法 (Matt Straathof) - Momento + https://www.gomomento.com/jp/resources/blog-jp/how-we-turned-up-the-heat-on-node-js-lambda-cold-starts/ +- `excludeNodeModulesFromSourceMapPlugin`の元ネタ + https://github.com/evanw/esbuild/issues/1685#issuecomment-944928069 \ No newline at end of file diff --git a/src/content/tags/aws-cdk.json b/src/content/tags/aws-cdk.json new file mode 100644 index 0000000..d9f2786 --- /dev/null +++ b/src/content/tags/aws-cdk.json @@ -0,0 +1,16 @@ +{ + "name": "AWS CDK", + "description": "AWS Cloud Development Kit(AWS CDK)は、プログラミング言語を使用してインフラストラクチャをコードとして定義できるツールです。クラウドリソースを効率的かつ再利用可能に構築・管理するための強力なツールセットを提供します。", + "image": "./aws-cdk.png", + "fullSizeImage": true, + "links": [ + { + "text": "aws-cdk - GitHub", + "url": "https://github.com/aws/aws-cdk" + }, + { + "text": "AWS Cloud Development Kit - AWS", + "url": "https://aws.amazon.com/jp/cdk/" + } + ] +} diff --git a/src/content/tags/aws-cdk.png b/src/content/tags/aws-cdk.png new file mode 100644 index 0000000000000000000000000000000000000000..b4efc5f1cc640d7028a2fea7dd39bae7100b92e8 GIT binary patch literal 936 zcmV;Z16TZsP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw0000p zP)t-s|NsB-^7E{*wr6j8OjTkdFFtjAhU4YuFgr<+m!8PW&$_v z3=IYjf}I|p0007ANklo_iNm@>fM{hIMnei`n5PUL}R`H_%qic=+k?|{C+`KV@%UA z9L>`_>SP_5g01D=K+W~SH{fjfPfUWZ7Mzho0NX28CPxY9L{?S%6JNGghM-$PvAhXBsCv!j({Sn)gm1R>KC2&Lt$s3 zyNhs`J$T-jg{WNc3W z*p~;`5%Cq^D#UL}1m?%MDN=unk&IX3FYeSUs`z>VGS72DnKeU&n^4u*eiOwM01osR z^T>;7+mOGG6Y0DO80Cza;=?L4fg{lcy)30sMv7bl+x?y6a*x-w5S`0~*-C^KfO@*u zS11`(F@+oh5Eav;h5%I&dGdrJuBog;fYh{m7-wz7Hrb;k%L?G34PRPBngIwz(}c>R zqw59$QiXaddHuZrN%V9KWK*&a@M2Q#s9qh0s1c-7*U~% z_5c6?C3HntbYx+4WjbSWWnpw>05UK#FfA}LEiyP%F)%tYH99ddD=;uRFfb2tky8Kw z03~!qSaf7zbY(hiZ)9m^c>ppnF*z+TIV~|aR53L=G&MRhG%GMLIxsL{>B5x&0000< KMNUMnLSTZDV2I2B literal 0 HcmV?d00001 diff --git a/src/content/tags/aws-lambda.json b/src/content/tags/aws-lambda.json new file mode 100644 index 0000000..3dc82f3 --- /dev/null +++ b/src/content/tags/aws-lambda.json @@ -0,0 +1,12 @@ +{ + "name": "Lambda", + "description": "AWS Lambdaは、サーバー管理の必要なくコードを実行できるサーバーレスコンピューティングサービスです。イベント駆動型で、自動スケーリングに対応し、使用したリソース分だけ課金されます。", + "image": "./aws-lambda.svg", + "fullSizeImage": true, + "links": [ + { + "text": "AWS Lambda - AWS", + "url": "https://aws.amazon.com/jp/lambda/" + } + ] +} diff --git a/src/content/tags/aws-lambda.svg b/src/content/tags/aws-lambda.svg new file mode 100644 index 0000000..16a563c --- /dev/null +++ b/src/content/tags/aws-lambda.svg @@ -0,0 +1,19 @@ + + + + + Icon-Architecture/64/Arch_AWS-Lambda_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/content/tags/aws.json b/src/content/tags/aws.json new file mode 100644 index 0000000..c3e9bb6 --- /dev/null +++ b/src/content/tags/aws.json @@ -0,0 +1,11 @@ +{ + "name": "AWS", + "description": "Amazon Web Services(AWS)は、クラウドコンピューティングのプラットフォームであり、ストレージ、計算能力、ネットワーキング、データベースなど、幅広いクラウドサービスを提供します。", + "image": "./aws.svg", + "links": [ + { + "url": "https://aws.amazon.com/jp/", + "text": "Amazon Web Services - ホーム" + } + ] +} diff --git a/src/content/tags/aws.svg b/src/content/tags/aws.svg new file mode 100644 index 0000000..656ef9e --- /dev/null +++ b/src/content/tags/aws.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + From bddd1704f09949fb05f8a5798d92f11689e34bc7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 05:55:35 +0000 Subject: [PATCH 2/5] [Bot] Update Blog Meta --- src/content/blog-metas/2024-12-01-esbuild-lambda-layer.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/content/blog-metas/2024-12-01-esbuild-lambda-layer.json diff --git a/src/content/blog-metas/2024-12-01-esbuild-lambda-layer.json b/src/content/blog-metas/2024-12-01-esbuild-lambda-layer.json new file mode 100644 index 0000000..9b7e801 --- /dev/null +++ b/src/content/blog-metas/2024-12-01-esbuild-lambda-layer.json @@ -0,0 +1,3 @@ +{ + "postDate": "2024-11-28T05:55:30.217Z" +} From 11ee88a9852a9b76efb6baea577f4323455a76cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 06:03:03 +0000 Subject: [PATCH 3/5] [Bot] Update Blog Meta --- src/content/blog-metas/2024-12-01-esbuild-lambda-layer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/blog-metas/2024-12-01-esbuild-lambda-layer.json b/src/content/blog-metas/2024-12-01-esbuild-lambda-layer.json index 9b7e801..fb424e1 100644 --- a/src/content/blog-metas/2024-12-01-esbuild-lambda-layer.json +++ b/src/content/blog-metas/2024-12-01-esbuild-lambda-layer.json @@ -1,3 +1,3 @@ { - "postDate": "2024-11-28T05:55:30.217Z" + "postDate": "2024-11-28T06:02:57.602Z" } From 6e28aee6b91f21dd442e1aeeeb0aa379eff66554 Mon Sep 17 00:00:00 2001 From: miyaji255 <84168445+miyaji255@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:09:58 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Advent=20Calendar=E3=81=AE=E3=82=BF?= =?UTF-8?q?=E3=82=B0=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...d-lambda-layer.json => 2024-11-28-esbuild-lambda-layer.json} | 0 ...build-lambda-layer.md => 2024-11-28-esbuild-lambda-layer.md} | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/content/blog-metas/{2024-12-01-esbuild-lambda-layer.json => 2024-11-28-esbuild-lambda-layer.json} (100%) rename src/content/blogs/{2024-12-01-esbuild-lambda-layer.md => 2024-11-28-esbuild-lambda-layer.md} (99%) diff --git a/src/content/blog-metas/2024-12-01-esbuild-lambda-layer.json b/src/content/blog-metas/2024-11-28-esbuild-lambda-layer.json similarity index 100% rename from src/content/blog-metas/2024-12-01-esbuild-lambda-layer.json rename to src/content/blog-metas/2024-11-28-esbuild-lambda-layer.json diff --git a/src/content/blogs/2024-12-01-esbuild-lambda-layer.md b/src/content/blogs/2024-11-28-esbuild-lambda-layer.md similarity index 99% rename from src/content/blogs/2024-12-01-esbuild-lambda-layer.md rename to src/content/blogs/2024-11-28-esbuild-lambda-layer.md index 82dfc79..bd94fa4 100644 --- a/src/content/blogs/2024-12-01-esbuild-lambda-layer.md +++ b/src/content/blogs/2024-11-28-esbuild-lambda-layer.md @@ -3,7 +3,7 @@ title: esbuildで効率の良いLambda Layerを作る description: "esbuildを使ってLambdaとLambda Layer用のバンドル方法をCDKを交えて説明します。" category: tech author: miyaji -tags: ["esbuild", "javascript", "aws", "aws-lambda", "aws-cdk"] +tags: ["advent-calendar", "esbuild", "javascript", "aws", "aws-lambda", "aws-cdk"] --- CDKでLambdaには`NodejsFunction`というバンドルとminifyを行うConstructがあるものの、Lambda Layerにはそのようなものがありません。そこで、esbuildを使ってLambdaとLambda Layerの両方のバンドルを行なっていきます。 From b80d470ce402e4ecad3554de6dc03d3a30fb9a05 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 06:11:52 +0000 Subject: [PATCH 5/5] [Bot] Update Blog Meta --- src/content/blog-metas/2024-11-28-esbuild-lambda-layer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/blog-metas/2024-11-28-esbuild-lambda-layer.json b/src/content/blog-metas/2024-11-28-esbuild-lambda-layer.json index fb424e1..d4e98eb 100644 --- a/src/content/blog-metas/2024-11-28-esbuild-lambda-layer.json +++ b/src/content/blog-metas/2024-11-28-esbuild-lambda-layer.json @@ -1,3 +1,3 @@ { - "postDate": "2024-11-28T06:02:57.602Z" + "postDate": "2024-11-28T06:11:46.788Z" }