Search code examples
node.jsaws-lambdaaws-cdk

Speed up nodejs lambda bundling with cdk


Building lambda dependent stacks take a very long time due to each lambda bundling every time the stack is built. A project using 50+ lambdas can be slowed down significantly by waiting for every lambda to bundle every single time.


Solution

  • There are active github issues dealing with this see here and here.

    The workaround I found was to bundle them in parallel before running cdk and add the bundler file to the cdk config to run before any cdk command.

    We were able to shorten our build times from 50+ seconds to >1 sec for all our lambdas. here is the build script:

    //bundleLambdas.ts
    import { build } from "esbuild";
    import * as glob from "glob";
    import * as path from "path";
    import { promises as fs } from "fs"; // Use the promises API of fs
    
    const handlersDir = "{PATH_TO_LAMBDA_FOLDER}";
    
    export async function bundleLambdas() {
      const start = performance.now();
      await clearDist();
    
      const entryFiles = (
        await Promise.all(
          glob.sync(`${handlersDir}/**/*.ts`, { dot: true }).map(async (file) => {
            return (await hasHandlerFunction(file)) ? file : null;
          }),
        )
      ).filter(Boolean); // This will filter out null values, leaving only files with exports.handler
    
      const buildTasks = entryFiles.map((file: any) => {
        const relativePath = path.relative(handlersDir, file);
        const directoryName = path.dirname(relativePath);
        const fileNameWithoutExtension = path.basename(file, ".ts");
    
        return build({
          entryPoints: [file],
          bundle: true,
          outdir: `./dist/${directoryName}/${fileNameWithoutExtension}`,
          platform: "node",
          external: ["@aws-sdk/*"],
        });
      });
    
      // Run all builds in parallel
      await Promise.all(buildTasks)
        .then(() => {
          const end = performance.now();
          const duration = (end - start) / 1000;
          console.error(`\n✨ Lambda Bundle time: ${duration.toFixed(2)}s\n`);
        })
        .catch((err) => {
          console.error("Build error:", err);
          process.exit(1);
        });
    }
    
    async function clearDist() {
      try {
        await fs.rm("./dist", { recursive: true });
      } catch (err) {
        console.error("Error clearing dist folder:");
      }
      try {
        await fs.mkdir("./dist", { recursive: true });
      } catch {
        console.error("Error creating dist folder:");
      }
    }
    
    async function hasHandlerFunction(file: any) {
      const content = await fs.readFile(file, "utf-8");
      return content.includes("exports.handler");
    }
    
    bundleLambdas();
    

    This is how we modified our cdk.json

    // cdk.json
    // bundle lambdas before running cdk root file
    {
    "app": "npx ts-node bin/bundleLambdas.ts && npx ts-node --prefer-ts-exts bin/cdk-main.ts",
    //...
    }
    

    And here is how we grabbed the pre-bundled Lambda:

    // use fromAsset to grab prebuilt lambda
    const lambdaFunction = new lambda.Function(this, ${id}`, {
          code: lambda.Code.fromAsset(`dist/${id}`),
          handler: `${id.split("/").pop()}.handler`,
          runtime: lambda.Runtime.NODEJS_18_X
    }