Search code examples
node.jsamazon-web-servicesserverlesslayer

Use custom Layers in NodeJS lambda


I get an error while trying to import my layers into my lambda function. I have checked all my settings and configs and I just dont understand why when my function is deployed and ran I get an error from AWS saying it cannot locate the layers layer:

{
    "errorType": "Error",
    "errorMessage": "Cannot find module 'layers'\nRequire stack:\n- /var/task/entities/transakWebhookEvent.js\n- /var/task/handler.js\n- /var/task/s_money_movements_api.js\n- /var/runtime/UserFunction.js\n- /var/runtime/index.js",
    "code": "MODULE_NOT_FOUND",
    "requireStack": [
        "/var/task/entities/transakWebhookEvent.js",
        "/var/task/handler.js",
        "/var/task/s_money_movements_api.js",
        "/var/runtime/UserFunction.js",
        "/var/runtime/index.js"
    ],
    "stack": [
        "Error: Cannot find module 'layers'",
        "Require stack:",
        "- /var/task/entities/transakWebhookEvent.js",
        "- /var/task/handler.js",
        "- /var/task/s_money_movements_api.js",
        "- /var/runtime/UserFunction.js",
        "- /var/runtime/index.js",
        "    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:815:15)",
        "    at Module._require.i.require (/var/task/serverless_sdk/index.js:9:73131)",
        "    at require (internal/modules/cjs/helpers.js:74:18)",
        "    at Object.<anonymous> (/var/task/entities/transakWebhookEvent.js:5:20)",
        "    at Module._compile (internal/modules/cjs/loader.js:999:30)",
        "    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)",
        "    at Module.load (internal/modules/cjs/loader.js:863:32)",
        "    at Function.Module._load (internal/modules/cjs/loader.js:708:14)",
        "    at Module.require (internal/modules/cjs/loader.js:887:19)",
        "    at Module._require.i.require (/var/task/serverless_sdk/index.js:9:73397)"
    ]
}

I am using the serverless framework to manage the deployment of my lambda and layers.

My project is setup like this:

05-24 07:12PM ~/poc $ ls
README.md  webapp-display  webapp-layers  webapp-money-movements  webapp-signup

I have my layers in a seperate folder with this folder structure:

05-24 07:09PM ~/poc/webapp-layers $ ls
index.js  layers  node_modules  package-lock.json  package.json  serverless.yml

my modules that I am trying to user my layers with are in this folder strucure:

05-24 07:10PM ~/poc/webapp-money-movements $ ls
entities  handler.js  node_modules  package-lock.json  package.json  prettier.config.js  serverless.yml

the package.json of my lambda module references the layers like this:

 {
  "name": "webapp-money-movements",
  "version": "1.0.0",
  "description": "",
  "dependencies": {
    "bcrypt": "^5.0.1",
    "express": "^4.17.1",
    "jsonwebtoken": "^8.5.1",
    "serverless-domain-manager": "^5.1.0",
    "serverless-http": "^2.7.0",
    "serverless-plugin-git-variables": "^5.1.0",
    "uuid": "^8.3.2"
  },
  "devDependencies": {
    "@serverless/eslint-config": "^3.0.0",
    "eslint": "^7.22.0",
    "eslint-config-strongloop": "^2.1.0",
    "eslint-plugin-import": "^2.22.1",
    "git-list-updated": "^1.2.1",
    "prettier": "^2.2.1",
    "layers": "file:../webapp-layers"
  },
  "eslintConfig": {
    "extends": "@serverless/eslint-config/node",
    "root": true
  },
  "scripts": {
    "lint": "eslint  --ignore-path .gitignore .",
    "lint:updated": "pipe-git-updated --ext=js -- eslint --ignore-path .gitignore",
    "prettier-check": "prettier -c --ignore-path .gitignore \"**/*.{css,html,js,json,md,yaml,yml}\"",
    "prettier-check:updated": "pipe-git-updated --ext=css --ext=html --ext=js --ext=json --ext=md --ext=yaml --ext=yml -- prettier -c",
    "prettify": "prettier --write --ignore-path .gitignore \"**/*.{css,html,js,json,md,yaml,yml}\"",
    "prettify:updated": "pipe-git-updated --ext=css --ext=html --ext=js --ext=json --ext=md --ext=yaml --ext=yml -- prettier --write"
  }
}

the serverless.yml of my lambda function is setup like so:

org: rt
app: poc
service: poc-money-movements

frameworkVersion: '2'

custom:
  tableNameDeposits: 'deposits-table-${self:provider.stage}'
  tableNameWithdrawalRequests: 'withdrawal-request-table-${self:provider.stage}'
  tableFundingAccountTransactions: 'funding-account-transaction-table-${self:provider.stage}'
  defaultRegion: eu-west-2
  [...]
  customStage:
    prod: prod
    staging: staging
    dev: dev
  [...]

plugins:
  - serverless-domain-manager

provider:
  name: aws
  runtime: nodejs12.x
  lambdaHashingVersion: '20201221'
  stage: ${self:custom.customStage.${opt:stage, 'dev'}, self:custom.customStage.dev}
  region: ${opt:region, self:custom.defaultRegion}
  profile: rt-${self:provider.stage}
  apiGateway:
    shouldStartNameWithService: true
  environment:
    WITHDRAWAL_REQUEST_TABLE: ${self:custom.tableNameWithdrawalRequests}
    DEPOSITS_TABLE: ${self:custom.tableNameDeposits}
    FUNDING_ACCOUNT_TRANSACTION: ${self:custom.tableFundingAccountTransactions}
  iam:
    role:
      statements:
        [...]
  

functions:
  money-movements-api:
    handler: handler.handler
    layers:
      - ${cf:poc-layers-${self:provider.stage}.AccountsLambdaLayerQualifiedArn}
    description: All endpoints needed for users to move money
    events:
      - http: ANY /
      - http: 'ANY /{proxy+}'

here is how I try to import my layers in my nodejs lambda function:

// Load the AWS SDK for JS
const AWS = require("aws-sdk");
AWS.config.update({region: 'eu-west-2'});
const jwt = require('jsonwebtoken');
const {accounts} = require("layers");

module.exports.new = async function(body) {
    let order = transakWebhook(body)
    console.log("order: ", order)

    const account = new accounts(order.partnerCustomerId)
[...]

the package.json of my layers module is setup like this

{
  "name": "webapp-layers",
  "version": "1.0.0",
  "description": "AWS Lambda Layers for Node.js",
  "main": "index.js",
  "scripts": {
    "compile": "tsc",
    "compile:watch": "npm run compile -- --watch"
  },
  "author": "Remi Tuyaerts",
  "license": "ISC",
  "dependencies": {
    "@middy/core": "^1.5.2",
    "@onfido/api": "^1.5.4",
    "auth0": "2.29.0",
    "awilix": "^4.3.3",
    "aws-sdk": "2.813.0",
    "compose-function": "^3.0.3",
    "date-fns": "2.16.1",
    "dotenv": "8.2.0",
    "ejs": "3.1.5",
    "fs-extra": "^9.1.0",
    "html-pdf": "2.2.0",
    "http-status": "^1.5.0",
    "joi": "17.2.1",
    "jsdom": "16.4.0",
    "knex": "0.21.14",
    "moment": "2.28.0",
    "node-fetch": "2.6.1",
    "nodemailer": "^6.4.17",
    "pg": "^8.4.2",
    "reflect-metadata": "^0.1.13",
    "typeorm": "^0.2.31",
    "winston": "3.3.3"
  },
  "devDependencies": {
    "@types/auth0": "^2.33.2",
    "@types/aws-lambda": "^8.10.72",
    "@types/jsdom": "^16.2.7",
    "@types/node": "^14.14.34",
    "@types/node-fetch": "^2.5.8",
    "@types/nodemailer": "^6.4.1",
    "typescript": "4.2.3"
  }
}

ther serverless.yml of my layers is setup like this:

org: rt
app: poc
service: poc-layers

provider:
  name: aws
  runtime: nodejs12.x
  lambdaHashingVersion: 20201221
  versionFunctions: false
  stage: ${self:custom.customStage.${opt:stage, 'dev'}, self:custom.customStage.dev}
  region: eu-west-2
  environment:
    STAGE: ${self:provider.stage}

custom:
  tableNameDeposits: 'deposits-table-${self:provider.stage}'
  tableNameWithdrawalRequests: 'withdrawal-request-table-${self:provider.stage}'
  tableFundingAccountTransactions: 'funding-account-transaction-table-${self:provider.stage}'
  customStage:
    prod: prod
    staging: staging
    dev: dev

layers:
  accounts:
    name: accounts
    path: layers/
    retain: true

my accounts.js in the folder ./webapp-layers/layers/ is setup like this:

const AWS = require("aws-sdk");
AWS.config.update({region: 'eu-west-2'});

const { v4: uuidv4 } = require('uuid');

// Create the DynamoDB Document Client
var dynamodb = new AWS.DynamoDB.DocumentClient();
var tableName = process.env.DEPOSITS_TABLE;

class accounts {
    constructor(userId) {
        this.userId = userId
    }
    
[...]

module.exports = { accounts };

I also export my layers in the index.js at the root of the ./webapp-layers/ like so:

"use strict";
const accounts = require("./layers/accounts");
exports.accounts = accounts;

Solution

  • At first, about the layer folder structure, all your shared files have to store under nodejs folder. This means, your layer will look like this:

    05-24 07:09PM ~/poc/webapp-layers $ ls index.js layers node_modules package-lock.json package.json serverless.yml

    ~/poc/webapp-layers $ tree
    ...
    layers/
       nodejs/
          accounts/
             index.js
    ...
    
    

    But, with the above structure, you have to update your lambda function to require accounts modules

    const accounts = require("/opt/nodejs/accounts");
    

    If you want to use

    const { accounts } = require("layers");
    

    the structure should be:

    ~/poc/webapp-layers $ tree
    ...
    layers/
       nodejs/
          node_modules/   <---------------
              layer/
                 index.js
          accounts/
             index.js
    

    And layer/index.js just re-export accounts module

    "use strict";
    const accounts = require("../accounts");
    exports.accounts = accounts;