Tar
Tar

Reputation: 9045

TypeScript on AWS Lambda: to bundle imports (how?) or not to bundle? or: Runtime.ImportModuleError: Cannot find module '@aws-sdk/..."

I have the following lambda.ts code I'm trying to make running on an AWS Lambda:

import 'aws-sdk'
import { /* bunch of stuff... */ } from "@aws-sdk/client-cloudwatch-logs";
import {Context, APIGatewayProxyResult} from 'aws-lambda';
import {DateTime} from "luxon";

export const lambdaHandler = async (event: any, context: Context): Promise<APIGatewayProxyResult> => {
    /* ... stuff ... */
}

which gets transpiled to:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.lambdaHandler = void 0;
require("aws-sdk");
//import {CloudWatchClient} from "@aws-sdk/client-cloudwatch";
const client_cloudwatch_logs_1 = require("@aws-sdk/client-cloudwatch-logs");
const luxon_1 = require("luxon");
const lambdaHandler = async (event, context) => {
    /* ... transpiled stuff ... */
}

When hitting the button, I'm getting this Response:

{
  "errorType": "Runtime.ImportModuleError",
  "errorMessage": "Error: Cannot find module '@aws-sdk/client-cloudwatch-logs'\nRequire stack:\n- /var/task/lambda.js\n- /var/runtime/index.mjs",
  "trace": [
    "Runtime.ImportModuleError: Error: Cannot find module '@aws-sdk/client-cloudwatch-logs'",
    "Require stack:",
    "- /var/task/lambda.js",
    "- /var/runtime/index.mjs",
    "    at _loadUserApp (file:///var/runtime/index.mjs:951:17)",
    "    at async Object.UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:976:21)",
    "    at async start (file:///var/runtime/index.mjs:1137:23)",
    "    at async file:///var/runtime/index.mjs:1143:1"
  ]
}

I played a lot with tsconfig.json, trying many things from Google / GitHub / SO / Rumors / Astrology / Numerology / Praying (monotheistic, pantheon-dwelling, neither..), but it only made me more confused.

For instance:

  1. Using the tsconfig.json from Building Lambda functions with TypeScript still emits a single transpiled .js file without embedding the imported @aws-sdk/client-cloudwatch-logs module in the emitted output lambda.js file
  2. Installing the AWS Common Runtime (CRT) Dependency states that I need to npm install @aws-sdk/... (naturally), but doesn't explain anything beyond, which makes me think that maybe I shouldn't bundle them at all, but simply import them (in the assumption that they are pre-defined/loaded in AWS's Lambda's runtime)

Runtime is Node.js 16.x, Handler is lambda.lambdaHandler (emitted file is called lambda.js), and this is my current tsconfig.json:

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "Node",
    "target": "ES2022",
    "sourceMap": true,
    "lib": [
      "ES2021"
    ],
    "typeRoots": ["node_modules/@types"],
    "outDir": "build",
    "baseUrl": "src",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "inlineSources": true,
    "rootDir": "src",
    "preserveConstEnums": true,
    "isolatedModules": true,
    "incremental": true,
    "importHelpers": true
  },
  "exclude": [
    "node_modules",
    "**/*.test.ts"
  ]
}

So I'm trying to understand:

  1. Do I even need to bundle those imported modules (such as @aws-sdk/client-cloudwatch-logs) at all, or they are already loaded by AWS Lambda's runtime?
  2. If I do need to bundle, then how? do I need to use some bundler or is it just a matter of configuring tsconfig.json properly?
  3. If bundler isn't mandatory, then how do I setup tsconfig.json to emit those 3rd-party modules?
  4. If a bundler is mandatory, then can they all fit (WebPack, Babel, etc..)? or since no frontend (index.html) is involved, then not all of them can fit?

Upvotes: 0

Views: 2598

Answers (1)

Andrew Gillis
Andrew Gillis

Reputation: 3915

  1. AWS SDK for JavaScript v3 (AKA modular) is not installed globally in the lambda execution context. You are using a v3 module (@aws-sdk/client-cloudwatch-logs) which is why it fails. AWS SDK v2 is installed globally, you are also using it (aws-sdk) so that require works fine.

  2. You should use a bundler like webpack, esbuild, parcel or rollup. If you are using AWS CDK, there is a nodejs function construct that will do the bundling with esbuild for you.

  3. TS will only emit your compiled javascript. If you are depending on javascript found in your node_modules directory, simply include that directory in your deployment package.

  4. Generally, bundlers will take your application entry points (main.js, handler.js, whatever you want really) and recursively resolve all the dependencies, tree-shake any unreachable code then create one file for each entry point that has no other external dependencies. There is a runtime performance cost to this of course but it does simplify things and in a serverless context it isn't usually too impactful.

So, to resolve your error you can take one of two approaches:

  1. Include your node_modules directory in your deployment package. (trivial)
  2. Use a bundler or CDK (more complex)

Note that in either case, you need to be careful about dependencies with native bindings (binaries basically) as the one installed on your dev machine likely isn't supported in the lambda environment.

Upvotes: 1

Related Questions